來看一道最大流模板水題,借這道題來學習一下最大流的幾個算法。
分別用Edmond-Karp,Dinic ,SAP來實現最大流算法。
從運行結過來看明顯SAP+當前弧優化+gap優化速度最快。
hiho一下 第115周:網絡流一•Ford-Fulkerson算法
原題網址:http://hihocoder.com/contest/hiho115/problem/1
網絡流一·Ford-Fulkerson算法
時間限制:10000ms
單點時限:1000ms
內存限制:256MB
描述
小Hi和小Ho住在P市,P市是一個很大很大的城市,所以也面臨着一個大城市都會遇到的問題:交通擁擠。
小Ho:每到周末回家感覺堵車都是一種煎熬啊。
小Hi:平時交通也還好,只是一到上下班的高峰期就會比較擁擠。
小Ho:要是能夠限制一下車的數量就好了,不知道有沒有辦法可以知道交通系統的最大承受車流量,這樣就可以限制到一個可以一直很順暢的數量了。
小Hi:理論上是有算法的啦。早在1955年,T.E.哈里斯就提出在一個給定的網絡上尋求兩點間最大運輸量的問題。並且由此產生了一個新的圖論模型:網絡流。
小Ho:那具體是啥?
小Hi:用數學的語言描述就是給定一個有向圖G=(V,E),其中每一條邊(u,v)均有一個非負數的容量值,記為c(u,v)≥0。同時在圖中有兩個特殊的頂點,源點S和匯點T。
舉個例子:
其中節點1為源點S,節點6為匯點T。
我們要求從源點S到匯點T的最大可行流量,這個問題也被稱為最大流問題。
在這個例子中最大流量為5,分別為:1→2→4→6,流量為1;1→3→4→6,流量為2;1→3→5→6,流量為2。
小Ho:看上去好像挺有意思的,你讓我先想想。
提示:Ford-Fulkerson算法
輸入
第1行:2個正整數N,M。2≤N≤500,1≤M≤20,000。
第2..M+1行:每行3個整數u,v,c(u,v),表示一條邊(u,v)及其容量c(u,v)。1≤u,v≤N,0≤c(u,v)≤100。
給定的圖中默認源點為1,匯點為N。可能有重復的邊。
輸出
第1行:1個整數,表示給定圖G的最大流。
樣例輸入
6 7
1 2 3
1 3 5
2 4 1
3 4 2
3 5 3
4 6 4
5 6 2
樣例輸出
5
一、Ford-Fulkerson算法
算法講解與圖片均摘自:http://hihocoder.com/contest/hiho115/problem/1
設f(u,v)實際流量,c(u,v)為每條路徑的容量。
整個圖G的流網絡滿足3個性質:
1. 容量限制:對任意u,v∈V,f(u,v)≤c(u,v)。
2. 反對稱性:對任意u,v∈V,f(u,v) = -f(v,u)。
3. 流守恆性:對任意u,若u不為S或T,一定有∑f(u,v)=0,(u,v)∈E。
對於上面例子中的圖,其對應的實際流量f網絡圖為:
其中綠邊表示例子中每條邊實際使用的流量f(u,v),虛線表示實際不存在的邊(v,u)。
在此基礎上,假設我們用cf(u,v)來表示c(u,v)-f(u,v),則可以表示每一條邊還剩下多少的流量可以使用,我們稱為殘留容量。
假設一條邊(u,v),其容量為3,即c(u,v)=3,由於邊(u,v)單向,(v,u)容量為0,c(v,u)=0。
使用了流量f(u,v)=2(同時有f(v,u)=-2)
則可以表示為:cf(u,v)= c(u,v)-f(u,v)=1, cf(v,u)= c(v,u)- f(v,u)=2。
由cf(u,v)構成的圖我們稱為殘留網絡。
比如例子中的殘留網絡圖為:
殘留網絡表示還可以使用的流量。
如果能從殘留網絡中找出一條從S到T的路徑p,使得路徑p上所有邊的cf(u,v)都大於0,假設路徑p上最小的cf(u,v)等於k,就可以使得S到T增加k的流量。
通過該條路徑p使得圖G的最大流得到了增加,這樣的路徑p被稱為增廣路徑。
Ford-Fulkerson算法的流程:
1. 將最初的圖G轉化為殘留網絡。
2. 在殘留網絡上尋找增廣路徑。
l 若存在增廣路徑,最大流量增加,同時對增廣路徑上的邊cf(u,v)進行修改(總流量增加,路徑上各邊容量相應減少,反向邊容量相應增加),再重復尋找增廣路徑。
l 若不存在增廣路徑,則這個圖不能再增加流量了,得到最大流。
Ford-Fulkerson算法確定了解決最大流問題的基本思路,接下來的關鍵就是算法的實現,如何尋找增廣路並實現路徑的修改。
二、Edmond-Karp算法
Edmond-Karp算法的思路其實就是Ford-Fulkerson算法。
Edmond-Karp流程:
1. 將最初的圖G轉化為殘留網絡。
2. 使用BFS反復尋找源點到匯點之間的增廣路徑。
若存在增廣路徑,對路徑上的流量進行相應修改(總流量增加,路徑上各邊容量相應減少,反向邊容量相應增加)。
3. 找不到增廣路時,當前的流量就是最大流。
#include <algorithm> #include <cstring> #include <string.h> #include <iostream> #include <list> #include <map> #include <set> #include <stack> #include <queue> #include <string> #include <utility> #include <vector> #include <cstdio> #include <cmath> #define LL long long #define N 40005 using namespace std; const int maxn=505; const int inf=0x7fffffff; struct Edge{ int u,v,c; int next; }edge[N]; int cnt;//邊數 int head[N]; void addedge(int u,int v,int c) { edge[cnt].u=u; edge[cnt].v=v; edge[cnt].c=c; //正向邊初始化為容量 edge[cnt].next=head[u]; head[u]=cnt++; edge[cnt].u=v; edge[cnt].v=u; edge[cnt].c=0; //反向邊容量初始化為0 edge[cnt].next=head[v]; head[v]=cnt++; } bool visit[maxn]; // 記錄結點i是否已訪問 int pre[maxn]; //記錄路徑 int m,n; int source,sink; //源點,匯點 bool bfs() //尋找從源點到匯點的增廣路,若找到返回true { queue<int>q; memset(pre,-1,sizeof(pre)); memset(visit,false,sizeof(visit)); pre[source]=-1; visit[source]=true; q.push(source); while(!q.empty()) { int u=q.front(); q.pop(); for(int i=head[u];i!=-1;i=edge[i].next) { int v=edge[i].v; if(edge[i].c>0&&!visit[v]) { pre[v]=i; visit[v]=true; if(v==sink) return true; //存在增廣路 q.push(v); } } } return false; } int Edmond_Karp() { int maxflow=0; int delta; while(bfs()) //反復在源點到匯點間尋找增廣路 { delta=inf; int i=pre[sink]; while(i!=-1) { delta=min(delta,edge[i].c); //路徑上最小的容量為流量增量 i=pre[edge[i].u]; } i=pre[sink]; while(i!=-1) { // 路徑上各邊容量相應減少,反向邊容量相應增加,總流量增加 edge[i].c-=delta; //增廣路上的邊減去使用的容量 edge[i^1].c+=delta; //同時相應的反向邊增加殘余容量 i=pre[edge[i].u]; } maxflow+=delta; } return maxflow; } int main() { while(scanf("%d%d",&n,&m)!=EOF) { int u,v,w; memset(head,-1,sizeof(head)); for(int i=0;i<m;i++) { scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } source=1,sink=n; printf("%d\n",Edmond_Karp()); } return 0; }
三、Dinic算法
Dinic算法的流程:
利用BFS對殘余網絡分層。每個節點的層數就是源點到這個節點經過的最少邊數。
用DFS 尋找增廣路。DFS每向下走一步必到達層數+1的節點,(標記滿足dep[v]=dep[u]+1的邊(u,v)為允許弧,增廣路只走允許弧)。
找到增廣路並相應修改后,回溯后繼續尋找增廣路,回溯到源點且無法繼續,DFS結束
重復以上過程直到BFS分層到達不了匯點,結束。
Dinic算法《北京大學ACM暑期課講義-網絡流》講的挺清楚的
#include <algorithm> #include <cstring> #include <string.h> #include <iostream> #include <list> #include <map> #include <set> #include <stack> #include <queue> #include <string> #include <utility> #include <vector> #include <cstdio> #include <cmath> #define N 40005 using namespace std; int const inf = 0x3f3f3f3f; int const MAX = 505; struct Edge{ int u,v,c; int next; }edge[N]; int cnt;//邊數 int head[N]; void addedge(int u,int v,int c) { edge[cnt].u=u; edge[cnt].v=v; edge[cnt].c=c; edge[cnt].next=head[u]; head[u]=cnt++; edge[cnt].u=v; edge[cnt].v=u; edge[cnt].c=0; edge[cnt].next=head[v]; head[v]=cnt++; } int n, m; int dep[MAX]; //分層 int source,sink; //源點,匯點 int bfs()//BFS對殘余網絡分層 { queue<int> q; while(!q.empty()) q.pop(); memset(dep, -1, sizeof(dep)); dep[source] = 0; //源點層數初始化為0 q.push(source); while(!q.empty()){ int u = q.front(); q.pop(); for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].v; if(edge[i].c> 0 && dep[v] == -1) { dep[v] = dep[u] + 1; q.push(v); } } } return dep[sink] != -1; //BFS分層是否能到達匯點 } int dfs(int u, int delta)//DFS 尋找增廣路,一次DFS可以尋找多條增廣路 { if(u == sink) //找到增廣路 return delta; int flow=0; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].v; if(edge[i].c> 0 && dep[v] == dep[u] + 1){ //dfs從前一層向后一層尋找增廣路 int tmp = dfs(v, min(delta-flow, edge[i].c)); // 路徑上各邊容量相應減少,反向邊容量相應增加,總流量增加 edge[i].c -= tmp; edge[i^1].c+= tmp; flow+=tmp; } } if(!flow) dep[u]=-1*inf; return flow; } int dinic() { int ans = 0, tmp; while(bfs()){ while(1){ tmp = dfs(1, inf); if(tmp == 0) break; ans += tmp; } } return ans; } int main() { while(~scanf("%d %d", &n, &m)){ cnt=0; memset(head,-1,sizeof(head)); int u, v, c; while(m--){ scanf("%d %d %d", &u, &v, &c); addedge(u,v,c); } source=1,sink=n; printf("%d\n", dinic()); } return 0; }
四、SAP 算法
基礎思路還是殘余網絡分層,尋找增廣路。和Dinic思路類似。
不過SAP分層只需要反向BFS一次。
關鍵在於Gap優化,當前弧優化。
Gap優化:
gap[i]表示dep[x]=i節點的個數。
如果一次重標號時,出現gap[i]=0,即出現斷層,則源點到匯點之間出現斷路,到達不了,結束算法。
當前弧優化:
對於每個點保存“當前弧”。
當前弧初始化是鄰接表的第一條弧,即head[i],查找邊的過程中找到一條允許弧,允許弧設為當前弧。
搜索邊的過程從當前弧開始搜,因為可以保證每個點當前弧之前的邊都不是允許弧。
代碼參考:http://blog.csdn.net/sprintfwater/article/details/7913181
#include <algorithm> #include <cstring> #include <string.h> #include <iostream> #include <list> #include <map> #include <set> #include <queue> #include <string> #include <utility> #include <vector> #include <cstdio> #include <stdio.h> #include <cmath> #define LL long long #define N 40005 using namespace std; const int maxn=505; const int inf=0x7fffffff; struct Edge{ int u,v,c; int next; }edge[N]; int cnt; int head[N]; void addedge(int u,int v,int c) { edge[cnt].u=u; edge[cnt].v=v; edge[cnt].c=c; edge[cnt].next=head[u]; head[u]=cnt++; edge[cnt].u=v; edge[cnt].v=u; edge[cnt].c=0; edge[cnt].next=head[v]; head[v]=cnt++; } int m,n; int source,sink; //源點,匯點 int gap[maxn]; //gap優化 int dep[maxn]; //層數 int cur[maxn]; //當前弧優化 int path[maxn]; //用一個棧儲存增廣路路徑 void rev_bfs() //對殘余網絡逆向分層 { memset(dep,-1,sizeof(dep)); memset(gap,0,sizeof(gap)); queue<int>q; dep[sink]=0; //匯點sink的深度為0 gap[0]=1; // 層數為0的點有1個 q.push(sink); while(!q.empty()) { int u=q.front(); q.pop(); for(int i=head[u];i!=-1;i=edge[i].next) { int v=edge[i].v; if(edge[i^1].c>0&&dep[v]==-1) { q.push(v); dep[v]=dep[u]+1; gap[dep[v]]++; } } } } int SAP() { rev_bfs(); //只需要bfs分層一次,之后的層數更新不用重新bfs // for(int i=1;i<=n;i++) cout<<dep[i]<<endl; memcpy(cur, head,sizeof(cur)); //當前弧初始化是鄰接表的第一條弧,即head[i] int maxflow = 0; int u=source; int top=0; int i; while (dep[source] < n) //最大的層數只會是n,如果大於等於n說明中間已經斷層了 { if (u==sink) //找到了一條增廣路,則沿着增廣路修改流量 { int delta=inf; int flag=n; //flag記錄增廣路上容量最小的邊 for (i=0; i!=top; i++){ if (delta>edge[path[i]].c) { delta=edge[path[i]].c; flag=i; } } for (i=0;i!=top;i++) // 路徑上各邊容量相應減少,反向邊容量相應增加,總流量增加 { edge[path[i]].c-=delta; edge[path[i]^1].c+=delta; } maxflow += delta; top = flag; //回溯到流量恰好變為0的最上層節點,繼續尋找增廣路 u = edge[path[top]].u; } for (i = cur[u]; i != -1; i = edge[i].next) { int v=edge[i].v; if (edge[i].c>0 && dep[u]==dep[v]+1) break; } if (i!=-1) //找到一條允許弧 { cur[u]=i; //允許弧設為當前弧 path[top++]=i; u=edge[i].v; } else //找不到允許弧,重新分層,再尋找增廣路 { //對u節點層數進行修改 if (--gap[dep[u]] == 0) break;// gap優化,如果出現斷層,結束算法 int mind = n+1; for (i = head[u]; i != -1; i = edge[i].next) //尋找可以增廣的最小層數 { if (edge[i].c>0 && mind>dep[edge[i].v]) { mind=dep[edge[i].v]; cur[u]=i; //允許弧設為當前弧 } } dep[u]=mind+1; //更新層數 gap[dep[u]]++; u=(u==source)? u : edge[path[--top]].u; //回溯 } } return maxflow; } int main() { while(~scanf("%d%d",&n,&m)) { int u,v,w; cnt=0; memset(head,-1,sizeof(head)); for(int i=0;i<m;i++) { scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } source=1,sink=n; printf("%d\n",SAP()); } return 0; }