最大流問題的幾種經典解法綜述


 一、什么是最大流問題 

假設現在有一個地下水管道網絡,有m根管道,n個管道交叉點,現在自來水廠位於其中一個點,向網絡中輸水,隔壁老王在另外一個點接水,已知由於管道修建的年代不同,有的管道能承受的水流量較大,有的較小,現在求在自來水廠輸入的水不限的情況下,隔壁老王能接到的水的最大值?

為解決該問題,可以將輸水網絡抽象成一個聯通的有向圖,每根管道是一條邊,交叉點為一個結點,從u流向v的管道能承受的最大流量稱為容量,設為cap[u][v],而該管道實際流過的流量設為flow[u][v],自來水廠稱為源點s,隔壁老王家稱為匯點t,則該問題求的是最終流入匯點的總流量flow的最大值。

 

 

 

二、思路分析

關於最大流問題的解法大致分為兩類:增廣路算法和預流推進算法。增廣路算法的特點是代碼量小,適用范圍廣,因此廣受歡迎;而預流推進算法代碼量比較大,經常達到200+行,但運行效率略高,如果腹黑的出題人要卡掉大多數人的code,那么預流推進則成為唯一的選擇。。。。⊙ ⊙ )

咳咳。。。先來看下增廣路算法:

為了便於理解,先引入一個引理:最大流最小割定理。

在一個連通圖中,如果刪掉若干條邊,使圖不聯通,則稱這些邊為此圖的一個割集。在這些割集中流量和最小的一個稱為最小割。

最大流最小割定理:一個圖的最大流等於最小割。

大開腦洞一下,發現此結論顯而易見,故略去證明(其實嚴格的證明反而不太好寫,但是很容易看出結論是對的,是吧)。這便是增廣路算法的理論基礎。

在圖上從st引一條路徑,給路徑輸入流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 }
View Code

 

 

四、Dicnic算法

計算機科學家Dinitz發明的算法,屬於增廣路算法的一種。與SAP的不同之處有:1.bfs預處理,按到s的距離划分層次圖,記錄在h數組里,每次尋找路徑只連相鄰距離的點;2.dfs代替bfs找增廣路,在尋找失敗時便於回溯,提高效率。應用也比較廣泛。

復雜度為O(m*n^2)

所以當n>>m時應該用SAPm>>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 }
View Code

 

 

五、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 }
View Code

 

 

六、測試數據

       輸入:

       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
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM