1. 程式人生 > 實用技巧 >網路流

網路流

網路流

網路流概念

   在一個有向圖上選擇一個源點,一個匯點,每一條邊上都有一個流量上限(以下稱為容量),

   即經過這條邊的流量不能超過這個上界,同時,除源點和匯點外,所有點的入流和出流都相等,

   而源點只有流出的流,匯點只有匯入的流。這樣的圖叫做網路流。

相關定義

1. 源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點。

  1. 匯點:另一個點也很特殊,只進不出,叫做匯點。

  2. 容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[i,j]表示,流量則通常是f[i,j].

  通常可以把這些邊想象成道路,流量就是這條道路的車流量,容量就是道路可承受的最大的車流量。

  很顯然的,流量<=容量。而對於每個不是源點和匯點的點來說,可以類比的想象成沒有儲存功能的貨物的中轉站,

  所有“進入”他們的流量和等於所有從他本身“出去”的流量。

4. 最大流:把源點比作工廠的話,問題就是求從工廠最大可以發出多少貨物,是不至於超過道路的容量限制,也就是,最大流。

增廣路演算法

  該方法通過尋找增廣路來更新最大流,有 EK,dinic,SAP,ISAP 主流演算法。

  求解最大流之前,我們先認識一些概念。

  最常用的就是dinic 但是費用流需要用到EK演算法,so,要學會EK演算法,dinic;

  增廣路:在圖中若一條從源點到匯點的路徑上所有邊的 剩餘容量都大於 0 (注意是大於不是大於等於),這條路被稱為增廣路。

求解思路

  從圖中找一條增廣路,然後增廣,怎麼找?

  1.從源點開始bfs,找到到匯點的一條路徑,並記錄這條路徑上所有邊剩餘流量最小值,因為要找增廣

  路,所以我們在bfs時要判斷一下邊的剩餘容量是否為0,記得用一個pre陣列記錄下路徑。

  2.找到路徑後,對其進行增廣(程式碼裡的up函式),增廣就是把這條路徑的每條邊都減去這

  些邊中剩餘流量的最小值(bfs時記錄),反向邊加上這個最小值(關於方向邊下面再解釋)。

  3.一直找增廣路增廣,直到不能增廣為止(找不到增廣路)。

  可以看下面這張圖。

上面我們提到了反向邊,下面我們解釋下為什麼要建反向邊。(放圖,簡單圖我還是可以的現學的)

 像我們上面這張圖,因為我們bfs時不能確定第一次走哪條邊,要是你像bmf一樣運氣不好,

 如果bfs第一次找到的增廣路是1→3→2→1的話,我們最後求得的最大流就是1.

 但是很明顯這張圖最大流是2,所以我們發現第一次找的增廣路不是最優的,這時候你就涼了。

 那我們怎麼解決呢反向邊,反向邊其實就是一個反悔的機會,也就是讓流過的流量流回去。

 如果還不明白的話還學什麼網路流,下面模擬一下這個過程。

 先說一下反向邊的一些東西,反向邊初始化為0,當正向邊剩餘流量減少的是時候,

 反向邊流量增加同樣的值,因為我們可以反悔的流量等於已經從這條邊正向流過的流量。

 下面看一下我們是如何通過反向邊反悔的又要做圖qaq。

 因為我不會畫反向邊,所以我們假設‘1/0’左邊那個數字表示正向變邊權,右邊是反向邊。

 下面這張圖就是建完邊後的圖。

 如果我們第一次找到的增廣路是1→3→2→1的話,總流量+1=1,圖就變成了

 我們發現我們還可以找到增廣路1→2→3→4,總流量+1=2,圖變成

 然後發現,找不到增廣路了,程式結束不用作圖了,我們發現

再找增廣路的過程中3→2這條邊正向一次,反向一次,相當於流量為0.

這就是反向邊的作用。

另外提供一種小技巧,使用鄰接表建圖的話,可以邊的編號從2開始建,我們知道

2^1=3,3^1=2……

這樣的話我們可以通過異或得到反向邊的編號(記得建完正向邊,緊接著就建反向邊),具體看程式碼

時間複雜度為O(nm2)至於為什麼,本人很菜不會,另外,一般時間複雜度是遠遠達不到這個值的。

程式碼(由於本人沒寫過EK,所以從同學那扒了一份)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define LL long long
using namespace std;
const int inf=1<<29;
const int N=207;
const int M=5e3+7;
int n,m,s,t,cnt=1;//從編號2開始建邊 
LL maxf;//最大流 
int head[N],vis[N],pre[N];
LL incf[N];
LL v[N][N]; 
struct edge{
    int v,nxt;
    LL w;
}e[M<<1];//因為要建反向邊,所以開二倍空間 
void add_edge(int u,int v,LL w){//存邊 
    cnt++;
    e[cnt].v=v;
    e[cnt].w=w;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
bool bfs(){
    memset(vis,0,sizeof(vis));
    queue<int>q;
    q.push(s);
    vis[s]=1;
    incf[s]=inf;
    while(q.size()){
        int now=q.front();q.pop();
        for(int i=head[now];i;i=e[i].nxt){
            if(e[i].w==0||vis[e[i].v])continue;
            int to=e[i].v;
            incf[to]=min(incf[now],e[i].w);//記錄路徑最小邊的流量 
            pre[to]=i;//記錄路徑邊的編號 
            q.push(to);
            vis[to]=1;
            if(to==t)return 1;
        }
    }
    return 0;
}
void up(){
    int x=t;
    while(x!=s){
        int i=pre[x];
         e[i].w-=incf[t];
         e[i^1].w+=incf[t];//反向邊加上正向邊減少的流量 
         x=e[i^1].v;
    }
    maxf+=incf[t];
} 
inline int read()
{
    int x = 0 , f = 1;  char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-')  f = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
    return x * f;
}
int main(){
    n = read(); m = read(); s = read(); t = read();
    for(int i=1;i<=m;i++){
        int x,y,z;
        x = read(); y = read(); z = read();
        v[x][y] += z;
    }
    for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) if(v[i][j]) add_edge(i,j,v[i][j]),add_edge(j,i,0);
    while(bfs())up();
    cout<<maxf<<endl;
}

//碼風過醜,請聯絡Aswert 這鍋我不背

Dinic 演算法

學了EK還學dinic有什麼用呢,有用,我們來分析下下面這張圖不小心把顏色改了下,懶得再做一張。

如果你運氣不好的話像bmf一樣,若你每次找到的增廣路都經過了2→3或3→2這條邊的話你又涼了

所以這時候就用到了我們的Dinic演算法。

Dinic 演算法 的過程是這樣的:每次增廣前,我們先用 BFS 來將圖分層。設源點的層數為1 ,

那麼一個點的層數便是它離源點的最近距離。

層次用陣列dep表示。分層圖對於每條邊滿足dep[v]=dep[u]+1。

我們思考,EK演算法每輪可能會遍歷整個圖,但只找出一條增廣路,屬於單路增廣。

而Dinic屬於多路增廣,時間複雜度O(n2m)

求解思路

1.bfs求出節點的層次,構建分層圖

2.對於求出的分層圖,用dfs進行多路增廣,由於本人菜,講的不是很明白,我們可以看程式碼

3.當前弧優化 :cur[]陣列的應用,如果一條邊已經被增廣過,那麼它就沒有可能被增廣第二次。那麼,我們下一次進行增廣的時候,

就可以不必再走那些已經被增廣過的邊。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
#define LL long long
const int INF = 2333333;
const int N = 210;
int n,m,x,y,w,s,t,tot = 1;
LL maxflow;
int dep[N],head[N];
struct node{
	int to , net , w;
}e[1000010];
inline int read()
{
	int s = 0 , w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-')w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s = s * 10+ch-'0'; ch = getchar();}
	return s * w;
}
void add(int x, int y , int w)
{
	e[++tot].w = w;
	e[tot].to = y;
	e[tot].net = head[x];
	head[x] = tot;
}
bool bfs()//構建分層圖 
{
	memset(dep , 0 , sizeof(dep));//每個節點層次初始化 
	queue<int> q;
	q.push(s); dep[s] = 1;//源點初始化為1; 
	while(!q.empty())
	{
		int x = q.front(); q.pop();
		for(int i = head[x]; i; i = e[i].net)
		{
			int to = e[i].to;
			if(e[i].w && !dep[to])//構建分層圖的時候要保證邊不為0,如果dep[]已經被更新就不用更新了 
			{
				q.push(to);
				dep[to] = dep[x] + 1;
				if(to == t) return 1;//如果到達匯點,進行dfs 
			}
		}
	}
	return 0;
}
int dinic(int x , int flow)
{
	if(x == t) return flow;
	int rest = flow , k;//rest表示當前這個節點最大允許通過流量 
	for(int i = head[x]; i && rest; i = e[i].net)
	{
		int to = e[i].to; int val = e[i].w;
		if(val && (dep[to] == dep[x] + 1))//尋找增廣路 
		{
			k = dinic(to , min(rest , val));
			if(!k) dep[to] = 0;//如果在to之後的路徑找不到增廣路,踢出分層圖 
			e[i].w -= k; e[i^1].w += k;
			rest -= k;//當前節點最大允許通過流量減去這次通過的流量 
		}
	}
	return flow - rest;
}
int main()
{
	n = read(); m = read(); s = read(); t = read();
	for(int i = 1; i <= m; i++)
	{
		x = read(); y = read(); w = read();
		add(x,y,w); add(y,x,0);
	}
	int flow = 0;
	while(bfs())
	{
		while(flow = dinic(s,INF)) maxflow += 1LL*flow;
	}
	printf("%lld\n",maxflow);
	return 0;
}

最小割

最小割問題是指:給出一種有向圖(無向圖)和兩個點s,t以及圖中的邊的邊權,

求一個權值和最小的邊集,使得刪除這些邊之後是s,t不連通。這類問題,一般運用最

大流等於最小流定理,求出最大流來解決。證明自行百度百科

程式碼同上。

ENDing!