一、什么是最大流問題
假設現在有一個地下水管道網絡,有m根管道,n個管道交叉點,現在自來水廠位於其中一個點,向網絡中輸水,隔壁老王在另外一個點接水,已知由於管道修建的年代不同,有的管道能承受的水流量較大,有的較小,現在求在自來水廠輸入的水不限的情況下,隔壁老王能接到的水的最大值?
為解決該問題,可以將輸水網絡抽象成一個聯通的有向圖,每根管道是一條邊,交叉點為一個結點,從u流向v的管道能承受的最大流量稱為容量,設為cap[u][v],而該管道實際流過的流量設為flow[u][v],自來水廠稱為源點s,隔壁老王家稱為匯點t,則該問題求的是最終流入匯點的總流量flow的最大值。

二、思路分析
關於最大流問題的解法大致分為兩類:增廣路算法和預流推進算法。增廣路算法的特點是代碼量小,適用范圍廣,因此廣受歡迎;而預流推進算法代碼量比較大,經常達到200+行,但運行效率略高,如果腹黑的出題人要卡掉大多數人的code,那么預流推進則成為唯一的選擇。。。。( ⊙ o ⊙ )
咳咳。。。先來看下增廣路算法:
為了便於理解,先引入一個引理:最大流最小割定理。
在一個連通圖中,如果刪掉若干條邊,使圖不聯通,則稱這些邊為此圖的一個割集。在這些割集中流量和最小的一個稱為最小割。
最大流最小割定理:一個圖的最大流等於最小割。
大開腦洞一下,發現此結論顯而易見,故略去證明(其實嚴格的證明反而不太好寫,但是很容易看出結論是對的,是吧)。這便是增廣路算法的理論基礎。
在圖上從s到t引一條路徑,給路徑輸入流flow,如果此flow使得該路徑上某條邊容量飽和,則稱此路徑為一條增廣路。增廣路算法的基本思路是在圖中不斷找增廣路並累加在flow中,直到找不到增廣路為止,此時的flow即是最大流。可以看出,此算法其實就是在構造最小割。

增廣路算法
而預流推進算法的思路比較奇葩(沒找到比較好的圖,只能自行腦補一下了。。= =#):
先將s相連的邊流至飽和,這種邊飽和的結點稱為活動點, 將這些活動點加入隊列,每次從中取出一個點u,如果存在一個相鄰點v是非活動點,則順着邊u->v 推流,直到u變為非活動點。重復此過程,直到隊列空,則此時圖中的流flow即是最大流。
三、SAP算法
最短增廣路算法(shortest arguement-path algorithm),簡稱SAP。目前應用最廣的算法,代碼簡短又很好理解,一般情況下效率也比較高。屬於增廣路算法的一種,特別之處是每次用bfs找的是最短的路徑,復雜度為O(n*m^2)。
代碼如下:
1 #include<stdio.h> 2 3 #include<string.h> 4 5 #include<queue> 6 7 #include<iostream> 8 9 using namespace std; 10 11 12 13 const int maxn = 300; 14 15 const int INF = 1000000+10; 16 17 18 19 int cap[maxn][maxn]; //流量 20 21 int flow[maxn][maxn]; //容量 22 23 int a[maxn]; //a[i]:從起點 s 到 i 的最小容量 24 25 int p[maxn]; //p[i]: 記錄點 i 的父親 26 27 28 29 int main() 30 31 { 32 33 int n,m; 34 35 while(~scanf("%d%d", &n,&m)) 36 37 { 38 39 memset(cap, 0, sizeof(cap)); //初始化容量為 0 40 41 memset(flow, 0, sizeof(flow)); // 初始化流量為 0 42 43 44 45 int x,y,c; 46 47 for(int i = 1; i <= n; i++) 48 49 { 50 51 scanf("%d%d%d", &x,&y,&c); 52 53 cap[x][y] += c; // 因為可能會出現兩個點有多條邊的情況,所以需要全部加起來 54 55 } 56 57 int s = 1, t = m; // 第一個點為源點, 第 n 個點為匯點 58 59 60 61 queue<int> q; 62 63 int f = 0; // 總流量 64 65 66 67 for( ; ; ) // BFS找增廣路 68 69 { 70 71 memset(a,0,sizeof(a)); // a[i]:從起點 s 到 i 的最小殘量【每次for()時 a[] 重新清 0 因此同時可做標記數組 vis】 72 73 a[s] = INF; // 起點殘量無限大 74 75 q.push(s); // 起點入隊 76 77 78 79 while(!q.empty()) // 當隊列非空 80 81 { 82 83 int u = q.front(); 84 85 q.pop(); // 取出隊首並彈出 86 87 for(int v = 1; v <= m; v++) if(!a[v] && cap[u][v] > flow[u][v]) //找到新節點 v 88 89 { 90 91 p[v] = u; 92 93 q.push(v); // 記錄 v 的父親,並加入 FIFO 隊列 94 95 a[v] = min(a[u], cap[u][v]-flow[u][v]); // s-v 路徑上的最小殘量【從而保證了最后,每條路都滿足a[t]】 96 97 } 98 99 } 100 101 102 103 if(a[t] == 0) break; // 找不到, 則當前流已經是最大流, 跳出循環 104 105 106 107 for(int u = t; u != s; u = p[u]) // 從匯點往回走 108 109 { 110 111 flow[p[u]][u] += a[t]; //更新正向流 112 113 flow[u][p[u]] -= a[t]; //更新反向流 114 115 } 116 117 f += a[t]; // 更新從 s 流出的總流量 118 119 120 121 } 122 123 printf("%d\n",f); 124 125 } 126 127 128 129 return 0; 130 131 }
四、Dicnic算法
計算機科學家Dinitz發明的算法,屬於增廣路算法的一種。與SAP的不同之處有:1.用bfs預處理,按到s的距離划分層次圖,記錄在h數組里,每次尋找路徑只連相鄰距離的點;2.用dfs代替bfs找增廣路,在尋找失敗時便於回溯,提高效率。應用也比較廣泛。
復雜度為O(m*n^2)。
所以當n>>m時應該用SAP,m>>n時用Dinic。
代碼如下:
1 //n個點,m條邊,源點編號1,匯點編號n 2 3 #include<cstdio> 4 5 #include<cstring> 6 7 #include<algorithm> 8 9 #define N 5005 10 11 #define M 10005 12 13 #define inf 999999999 14 15 using namespace std; 16 17 18 19 int n,m,s,t,num,adj[N],dis[N],q[N]; 20 21 struct edge 22 23 { 24 25 int v,w,pre; 26 27 }e[M]; 28 29 void insert(int u,int v,int w) 30 31 { 32 33 e[num]=(edge){v,w,adj[u]}; 34 35 adj[u]=num++; 36 37 e[num]=(edge){u,0,adj[v]}; 38 39 adj[v]=num++; 40 41 } 42 43 int bfs() 44 45 { 46 47 int i,x,v,tail=0,head=0; 48 49 memset(dis,0,sizeof(dis)); 50 51 dis[s]=1; 52 53 q[tail++]=s; 54 55 while(head<tail) 56 57 { 58 59 x=q[head++]; 60 61 for(i=adj[x];i!=-1;i=e[i].pre) 62 63 if(e[i].w&&dis[v=e[i].v]==0) 64 65 { 66 67 dis[v]=dis[x]+1; 68 69 if(v==t) 70 71 return 1; 72 73 q[tail++]=v; 74 75 } 76 77 } 78 79 return 0; 80 81 } 82 83 int dfs(int s,int limit) 84 85 { 86 87 if(s==t) 88 89 return limit; 90 91 int i,v,tmp,cost=0; 92 93 for(i=adj[s];i!=-1;i=e[i].pre) 94 95 if(e[i].w&&dis[s]==dis[v=e[i].v]-1) 96 97 { 98 99 tmp=dfs(v,min(limit-cost,e[i].w)); 100 101 if(tmp>0) 102 103 { 104 105 e[i].w-=tmp; 106 107 e[i^1].w+=tmp; 108 109 cost+=tmp; 110 111 if(limit==cost) 112 113 break; 114 115 } 116 117 else dis[v]=-1; 118 119 } 120 121 return cost; 122 123 } 124 125 int Dinic() 126 127 { 128 129 int ans=0; 130 131 while(bfs()) 132 133 ans+=dfs(s,inf); 134 135 return ans; 136 137 } 138 139 int main () 140 141 { 142 143 while(~scanf("%d%d",&m,&n)) 144 145 { 146 147 int u,v,w; 148 149 memset(adj,-1,sizeof(adj)); 150 151 num=0; 152 153 s=1; 154 155 t=n; 156 157 while(m--) 158 159 { 160 161 scanf("%d%d%d",&u,&v,&w); 162 163 insert(u,v,w); 164 165 } 166 167 printf("%d\n",Dinic()); 168 169 } 170 171 }
五、HLPP算法
最高標號預留推進算法(highest-label preflow-push algorithm),簡稱HLPP。屬於預流推進算法的一種,據說是目前已知的效率最高的一種,但代碼量大且不好理解,所以用的很少。其思路基本和上面預流推進一致,區別是:在進入算法之前先預處理:用一個bfs以對s的距離划分層次圖,記為h數組。
在活動點隊列取出u時,如果存在一個相鄰點v,使得h[v]=h[u]+1,才順着邊u->v 推流,如果在u變為非活動點之前,邊u->v已經飽和,則u要重新標號為h[v]=min{h[u]}+1,並加入隊列。復雜度僅為O(√m * n^2)。
代碼如下:
1 #include <stdio.h> 2 3 #include <string.h> 4 5 #include <queue> 6 7 #include <algorithm> 8 9 using namespace std; 10 11 12 13 /* 14 15 網絡中求最大流HLPP高度標號預流推進算法O(V^2*E^0.5) (無GAP優化簡約版) 16 17 參數含義: n代表網絡中節點數,第1節點為源點, 第n節點為匯點 18 19 net[][]代表剩余網絡,0表示無通路 20 21 earn[]代表各點的盈余 22 23 high[]代表各點的高度 24 25 返回值: 最大流量 26 27 */ 28 29 const int NMAX = 220; 30 31 const int INF = 0x0ffffff; 32 33 34 35 int earn[NMAX], net[NMAX][NMAX], high[NMAX]; 36 37 int n, m; 38 39 queue<int> SQ; 40 41 42 43 void push(int u, int v) 44 45 { 46 47 int ex = min(earn[u], net[u][v]); 48 49 earn[u] -= ex; 50 51 net[u][v] -= ex; 52 53 earn[v] += ex; 54 55 net[v][u] += ex; 56 57 } 58 59 60 61 void relable(int u) 62 63 { 64 65 int i, mmin = INF; 66 67 for(i=1; i<=n; i++) 68 69 { 70 71 if(net[u][i] > 0 && high[i] >= high[u]) 72 73 { 74 75 mmin = min(mmin, high[i]); 76 77 } 78 79 } 80 81 high[u] = mmin +1; 82 83 } 84 85 86 87 void discharge(int u) 88 89 { 90 91 int i, vn; 92 93 while(earn[u] > 0) 94 95 { 96 97 vn = 0; 98 99 for(i=1; i<=n && earn[u] > 0; i++) 100 101 { 102 103 if(net[u][i] > 0 && high[u] == high[i]+1) 104 105 { 106 107 push(u,i); 108 109 vn ++; 110 111 if(i != n) SQ.push(i); 112 113 } 114 115 } 116 117 if(vn == 0) relable(u); 118 119 } 120 121 } 122 123 124 125 void init_preflow() 126 127 { 128 129 int i; 130 131 memset(high,0,sizeof(high)); 132 133 memset(earn,0,sizeof(earn)); 134 135 while(!SQ.empty()) SQ.pop(); 136 137 high[1] = n+1; 138 139 for(i=1; i<=n; i++) 140 141 { 142 143 if(net[1][i] > 0) 144 145 { 146 147 earn[i] = net[1][i]; 148 149 earn[1] -= net[1][i]; 150 151 net[i][1] = net[1][i]; 152 153 net[1][i] = 0; 154 155 if(i != n) SQ.push(i); 156 157 } 158 159 } 160 161 } 162 163 164 165 int high_label_preflow_push() 166 167 { 168 169 int i,j; 170 171 init_preflow(); 172 173 while(!SQ.empty()) 174 175 { 176 177 int overp = SQ.front(); 178 179 SQ.pop(); 180 181 discharge(overp); 182 183 } 184 185 return earn[n]; 186 187 } 188 189 190 191 int main () 192 193 { 194 195 while(~scanf("%d%d",&m,&n)) 196 197 { 198 199 int u,v,w; 200 201 memset(net, 0, sizeof net); 202 203 while(m--) 204 205 { 206 207 scanf("%d%d%d",&u,&v,&w); 208 209 net[u][v] = w; 210 211 //net[v][u] = 0; 212 213 } 214 215 printf("%d\n",high_label_preflow_push()); 216 217 } 218 219 }
六、測試數據
輸入:
m n
u1 v1 w1
u2 v2 w2
....
um vm wm
輸出:
flow
n -> 點數 m -> 邊數
ui -> 第i條邊的起點
vi -> 第i條邊的終點
wi -> 第i條邊的容量
flow -> 最大流
樣例數據如下:
1 Input: 2 3 5 4 4 5 1 2 40 6 7 1 4 20 8 9 2 4 20 10 11 2 3 30 12 13 3 4 10 14 15 16 17 8 6 18 19 1 2 40 20 21 1 4 30 22 23 2 3 20 24 25 2 5 20 26 27 4 5 20 28 29 3 6 15 30 31 5 6 10 32 33 4 6 20 34 35 36 37 10 8 38 39 1 2 20 40 41 2 3 30 42 43 3 4 30 44 45 4 5 20 46 47 1 6 10 48 49 6 7 15 50 51 7 8 15 52 53 8 5 18 54 55 3 6 20 56 57 3 8 15 58 59 60 61 8 5 62 63 1 2 10 64 65 1 3 15 66 67 1 4 20 68 69 2 5 25 70 71 3 5 18 72 73 4 5 16 74 75 2 3 10 76 77 3 4 12 78 79 80 81 13 8 82 83 1 2 10 84 85 1 4 13 86 87 1 7 11 88 89 2 3 21 90 91 4 5 15 92 93 7 8 12 94 95 2 4 17 96 97 4 7 15 98 99 3 5 18 100 101 5 8 20 102 103 3 6 21 104 105 5 6 21 106 107 6 8 16 108 109 110 111 Output: 112 113 50 114 115 116 117 45 118 119 120 121 30 122 123 124 125 41 126 127 128 129 34
