網絡流算法基礎

將每條有向邊想象成傳輸物質的管道。每個管道都有一個固定的容量,可以看作是物質能流經該管道的最大速度(譬如可以想象爲水流和河槽)。頂點是管道間的連接點,除了源點(S,Source)和匯點(T,Target)以外,物質只流經這些頂點。而不聚集在頂點中。

     注:下文提到的數字,基本都可加單位 “單位流量”來理解。

一、網絡流基礎

     網絡流中的最大流研究的問題是:在不違背容量限制的條件下,把物質從源點傳輸到匯點的最大速率是多少?上邊的圖示中,還有些需要說明,設G = (V,E)是一個流網絡,其容量函數爲c(c(u, v),Capicity)。s 爲源點,t 爲匯點。G 的流是一個函數 f (Flow)。上圖中每條邊標記的是流網絡的容量 c(u, v),右圖中,G中的流 |f| = 19。圖中只顯示正網絡流。如果 f(u,v) > 0,則標記(u,v)爲 f(u,v)/ c(u,v)(斜槓僅僅用來區分流和容量,不表示相除)。如果 f(u,v)<= 0,邊(u,v)只標記它的容量。譬如(s,v1)這條邊,最大流量限制是16,但實際流過11,所以表示爲11 / 16。

    網絡流算法要基於三種思想:殘留網絡(Residual Network),增廣路徑(Augmenting Path)和割(Cut)。

     和開頭同一個例子,上圖,圖(b)就是殘留網絡,仔細觀察一下,應該就可以明白,譬如邊(s,v1),從 s 到 v1 流過11,還剩5,所以殘餘容量是5,當然,流量是守恆的,所以反向從 v1 到 s 流過11。剩餘的容量 + 反向平衡的流量共同構成了殘留網絡。對於名詞“殘留容量(Residual Capacity)”的定義:在不超過容量c(u,v)的條件下,從 u 到 v 之間可以壓入的額外網絡流量,就是(u,v)的殘留容量(Residual Capacity),公式定義是:cf(u,v) = c(u,v) – f(u,v)。而殘留網絡Gf = (V,Ef)。

     圖(b),陰影覆蓋的邊爲增廣路徑 p ,其殘留容量爲 cf(p) = cf(v2,v3) = 4。看圖應該就可以大概理解什麼是增廣路了。看圖可以發現 s 到 v2還可以通過 5,v2到v3還可以通過 4,v3到 t 還可以流 5,按照常識,也可得出,這條路徑還可以再流過 4 的結論。而從 s 輸送 4 到 t 的這條路就是增廣路。"增廣路徑 p”的定義:p 爲殘留網絡Gf中從 s 到 t 的一條簡單路徑。在不違反邊的容量限制條件下,增廣路徑上的每條邊(u,v)可以容納從 u 到 v 的某額外正網絡流。Starvae師兄在講解增廣路時,有一段通俗的描述:

     假如有這麼一條路,這條路從源點開始一直一段一段的連到了匯點,並且,這條路上的每一段都滿足流量<容量,注意,是嚴格的<,而不是<=。那麼,我們一定能找到這條路上的每一段的(容量-流量)的值當中的最小值delta。我們把這條路上每一段的流量都加上這個delta,一定可以保證這個流依然是可行流。這樣我們就得到了一個更大的流,他的流量是之前的流量+delta,而這條路就叫做增廣路。

另外,這裏得出的,計算增廣路流量的公式非常重要:cf(p) = min { cf(u,v):(u,v)在 p 上},因爲最大流算法的核心基本也就是尋找增廣路了。

最大流最小割定理:一個流是最大流,當且僅當它的殘留網絡不包含增廣路徑。

    流網絡的割(S,T)將V劃分爲 S 和 T = V – S兩部分,使得 s ∈ S,t ∈ T。如果 f 是一個流,則穿過割 (S,T)的淨流被定義爲 f (S,T)。割(S,T)的容量爲 c(S,T)。一個網絡的最小割就是網絡中所有割中最小容量的割。

     上圖中,流網絡的割(S = {s,v1,v2},T = {v3,v4,t}),S中的頂點是黑色,T中的頂點是白色。穿越(S,T)的淨流量爲:f(v1,v3) + f(v2,v3) + f(v2,v4) = 12 + (-4) + 11 = 19;容量爲:c(v1,v3) + c(v2,v4) = 12 + 14 = 26。可以看出,割的淨流由雙向的網絡流組成。而割的容量僅由 S 到 T 的邊計算而得。

     還有一個很有重要的知識:反向邊。如圖(a),1是源點,4是匯點。很明顯如果第一次迭代找到的增廣路是1→2→4和1→3→4,則最大流是2,但是如果第一次迭代找到的增廣路是1→2→3→4,那麼流量只有1,如圖(b)是殘留網絡,這時候,反向邊就起作用了,由於反向邊的原因,第二次迭代的時候,又找到一條增廣路1→2→3→4。這樣下來,總的流量還是2。還是Starvae師兄的解釋:

      當我們第二次的增廣路走 3→2 這條反向邊的時候,就相當於把 2→3 這條正向邊已經是用了的流量給“退”了回去,不走 2→3 這條路,而改走從 2 點出發的其他的路也就是 2→4。(有人問如果這裏沒有 2→4 怎麼辦,這時假如沒有 2→4 這條路的話,最終這條增廣路也不會存在,因爲他根本不能走到匯點)同時本來在 3→4 上的流量由 1→3→4 這條路來“接管”。而最終 2→3 這條路正向流量1,反向流量1,等於沒有流量。反向邊的作用就是給程序一個可以後悔的機會。—— 一語中的啊。(這句話我加的^^)


二、Ford-Fulkerson算法

     Ford-Fulkerson算法在實際中並不常用,但是它提供了一種思想:先找到一條從源點到匯點的增廣路徑,這條路徑的容量是其中容量最小的邊的容量。然後,通過不斷找增廣路,一步步擴大流量,當找不到增廣路時,就得到最大流了(最大流最小割定理)。可以看看Ford-Fulkerson算法的僞代碼,

FORD-FULKERSON(G, s, t)
1  for each edge (u, v) ∈ E[G]
2       do f[u, v] ← 0
3          f[v, u] ← 0
4  while there exists a path p from s to t in the residual network Gf
5      do cf(p) ← min {cf(u, v) : (u, v) is in p}
6         for each edge (u, v) in p
7             do f[u, v] ← f[u, v] + cf(p)
8                f[v, u] ← -f[u, v]

     從僞代碼可以看出,Ford-Fulkerson的核心過程就是:第4~8行的 while 循環反覆找出 Gf 中的增廣路徑 p,並把沿 p 的流 f 加上其殘留容量cf(p)。當不再有增廣路徑時,流 f 就是一個最大流。完整圖示:

   (f)最後在 while 循環測試的殘留網絡,它沒有增光路徑,所以(e)中顯示的流就是最大流。

三、壓入重標記push_relabel算法O(VE)

Push-relabel用到一個很有趣的概念一Preflow(前置流)他允許流進的量比流出的量還要多,有水來就先流進來,流不出去再說。

Push-relabel算法的流程如下: 

1 )我們先假定一個高度函數 h ( u ) ,他代表u點的高度,只有

h ( u )比較高的點才能夠將水流到h ( u )比較低的點; 


2 ) 在程序一開始的時候,讓source node的高度是n ( node數) ,  其它點的高度都是 0 ,這樣source node纔有足夠的高度可以流往其它地方 ; 


3 ) 然後, 讓source node往其它所有跟他直接相鄰的node ,都流水管寬度的水量 ( 流過去之後當然要計算剩下的網絡情形,即計算 residual  edges(殘留網絡)


4 )對所有active node( 目前有水的 node )做 relabel(重標記)的動作:  在當某個node明明有水,但是他所連出去的所有對象的 h ( u ) 都比他還高, 則讓他的h ( u ) 增加爲至少有一條水管可以流出去的量,也就是讓這個有水的 active node的高度變成比他連往的”高度最小的 n o d e ”+l , ( 流過去之後還是要計算剩下的網絡情形


5 )對所有可以做push(壓入)動作的node做push的動作。  所謂的Push動作是指 : 當某個node有水,並且他有可以流出去的邊, 且他剛好比可以流出去的那個點高度高一點點 ( 高度恰好比他高 1 ) ,那就把某個node 的水流過去,要流多少呢?以下兩者取 min。  流出去的水管的量( 也就是說,這個active node的水量很多,  足夠把這條水管塞滿( 飽和) ,這個時候就叫做saturating Push)(飽和壓入)某個 n o d e 現在的水量( 這個 n o d e的水量不足以把流出去的這個水管填滿,稱作non saturating Push(不飽和壓入) 


6 )重複Relabel和Push的工作,一直到沒有active node爲止,此時從source node所流出的總流量( P r e f l o w) ,就是這個圖的最大流量。 

Push-relabel  algorithm 提供了最大流另一方向的思考,且就效率而言,Push -Relabel的複雜度爲 o (vE )

這裏以POJ 1459爲例

  1. #define MIN INT_MIN 
  2. #define MAX INT_MAX 
  3. #define N 110 
  4. int min(int a,int b){return a>b?b:a;} 
  5. int c[N][N];//殘留容量 
  6. int ef[N];//頂點餘流 
  7. int h[N];//頂點高度 
  8. int n; 
  9. int push_relabel(int s,int t){ 
  10.     int i,j; 
  11.     int ans = 0; 
  12.     memset(h,0,sizeof(h)); 
  13.     h[s] = t+1;//源點初始高度 
  14.     memset(ef,0,sizeof(ef)); 
  15.     ef[s] = MAX;//源點初始餘流 
  16.     queue<int> qq; 
  17.     qq.push(s); 
  18.     while(!qq.empty()){ 
  19.         int u = qq.front(); 
  20.         qq.pop(); 
  21.         for(i=0;i<=t;i++){ 
  22.             int p; 
  23.             int v = i; 
  24.             if(c[u][v]<ef[u])p = c[u][v]; 
  25.             else p = ef[u]; 
  26.             if(p>0 && (u==s || h[u] == h[v] +1)){ 
  27.                 c[u][v] -= p; 
  28.                 c[v][u] += p; 
  29.                 if(v==t)ans+=p;//如果到達了匯點,就將流值加入到最大流中 
  30.                 ef[u] -= p; 
  31.                 ef[v] += p; 
  32.                 if(v!=s && v!=t)qq.push(v);//只有既不是源點也不是匯點才進隊 
  33.             } 
  34.         } 
  35.         //如果不是源點且仍有餘流,則重標記高度再進隊。 
  36.         //這裏只是簡單的將高度增加了一個單位,也可以像上面所說的一樣賦值爲最低的相鄰頂點的高度高一個單位 
  37.         if(u!= s && u!=t && ef[u]>0) { 
  38.             h[u]++; 
  39.             qq.push(u); 
  40.         } 
  41.     } 
  42.     return ans; 
  43. int main(){ 
  44.     int np,nc,m; 
  45.     while(scanf("%d%d%d%d",&n,&np,&nc,&m) != -1){ 
  46.         int s = n,t = n+1; 
  47.         int i,j; 
  48.         memset(c,0,sizeof(c)); 
  49.         char ss[30]; 
  50.         for(i=0;i<m;i++){ 
  51.             int u,v,w; 
  52.             scanf("%s",ss); 
  53.             sscanf(ss,"(%d,%d)%d",&u,&v,&w); 
  54.             c[u][v] += w; 
  55.         } 
  56.         for(i=0;i<np;i++){ 
  57.             int u,w; 
  58.             scanf("%s",ss); 
  59.             sscanf(ss,"(%d)%d",&u,&w); 
  60.             c[s][u] += w; 
  61.         } 
  62.         for(i=0;i<nc;i++){ 
  63.             int v,w; 
  64.             scanf("%s",ss); 
  65.             sscanf(ss,"(%d)%d",&v,&w); 
  66.             c[v][t] += w; 
  67.         } 
  68.         printf("%d\n",push_relabel(s,t)); 
  69.     } 
  70.     return 0; 

四、Edmonds-Karp(EK)算法  O(V*E*E)

     EK算法基於Ford-Fulkerson算法,唯一的區別是將第 4 行用BFS(廣度優先搜索)來實現對增廣路徑 p 的計算。EK算法僞代碼基本和上邊的Ford-Fulkerson算法一樣。類似用DFS實現的還有Dinic算法。它們都屬於SAP(Shortest Augmenting Path)算法,從英文即可看出,它們每次都在尋找最短增廣路。對於EK算法,每次用一遍 BFS 尋找從源點 s 到終點 t 的最短路作爲增廣路徑,然後增廣流量 f 並修改殘量網絡,直到不存在新的增廣路徑。E-K 算法的時間複雜度爲 O(VE^2),適用於稀疏邊,由於 BFS 要搜索全部小於最短距離的分支路徑之後才能找到終點,因此頻繁的 BFS 效率是比較低的。實踐中此算法使用的機會較少。

這裏以POJ 1273爲例,這裏可以作爲EK模板,也是我的第一道網絡流,mark一下

  1. #define MIN INT_MIN 
  2. #define MAX INT_MAX 
  3. #define N 204 
  4.  
  5. int c[N][N];//邊容量 
  6. int f[N][N];//邊實際流量 
  7. int pre[N];//記錄增廣路徑 
  8. int res[N];//殘餘網絡 
  9. queue<int> qq; 
  10. void init(){ 
  11.     while(!qq.empty())qq.pop(); 
  12.     memset(c,0,sizeof(c)); 
  13.     memset(f,0,sizeof(f)); 
  14. int EK(int s,int t){ 
  15.     int i,j; 
  16.     int ans=0; 
  17.     while(1){ 
  18.         memset(res,0,sizeof(res)); 
  19.         res[s] = MAX;//源點的殘留網絡要置爲無限大!否則下面找增廣路出錯 
  20.         pre[s] = -1; 
  21.         qq.push(s); 
  22.         //bfs找增廣路徑 
  23.         while(!qq.empty()){ 
  24.             int x = qq.front(); 
  25.             qq.pop(); 
  26.             for(i=1;i<=t;i++){ 
  27.                 if(!res[i] && f[x][i] < c[x][i]){ 
  28.                     qq.push(i); 
  29.                     pre[i] = x; 
  30.                     res[i] = min(c[x][i] - f[x][i], res[x]);//這裏類似dp,如果有增廣路,那麼res[t]就是增廣路的最小權 
  31.                 } 
  32.             } 
  33.         } 
  34.         if(res[t]==0)break;//找不到增廣路就退出 
  35.         int k = t; 
  36.         while(pre[k]!=-1){ 
  37.             f[pre[k]][k] += res[t];//正向邊加上新的流量 
  38.             f[k][pre[k]] -= res[t];//反向邊要減去新的流量,反向邊的作用是給程序一個後悔的機會 
  39.             k = pre[k]; 
  40.         } 
  41.         ans += res[t]; 
  42.     } 
  43.     return ans; 
  44. int main(){ 
  45.     int n,m; 
  46.     while(scanf("%d%d",&n,&m) != -1){ 
  47.         int i,j; 
  48.         init(); 
  49.         while(n--){ 
  50.             int a,b,v; 
  51.             scanf("%d%d%d",&a,&b,&v); 
  52.             c[a][b]+=v; 
  53.         } 
  54.         printf("%d\n",EK(1,m)); 
  55.     } 
  56.     return 0; 

四、Improved SAP(ISAP)算法

      ISAP字面意思是改良的最短增廣路算法。關於ISAP,一位叫 DD_engi 的神牛講非常清楚,引用一下:

     SAP算法(by dd_engi):求最大流有一種經典的算法,就是每次找增廣路時用BFS找,保證找到的增廣路是弧數最少的,也就是所謂的 Edmonds-Karp 算法。可以證明的是在使用最短路增廣時增廣過程不超過 V * E次,每次 BFS 的時間都是O(E),所以 Edmonds-Karp 的時間複雜度就是O(V * E^2)。

     如果能讓每次尋找增廣路時的時間複雜度降下來,那麼就能提高算法效率了,使用距離標號的最短增廣路算法就是這樣的。所謂距離標號,就是某個點到匯點的最少的弧的數量(另外一種距離標號是從源點到該點的最少的弧的數量,本質上沒什麼區別)。設點 i 的標號爲D[i],那麼如果將滿足D[i] = D[j] + 1的弧(i,j))叫做允許弧,且增廣時只走允許弧,那麼就可以達到“怎麼走都是最短路”的效果。每個點的初始標號可以在一開始用一次從匯點沿所有反向邊的BFS求出,實踐中可以初始設全部點的距離標號爲0,問題就是如何在增廣過程中維護這個距離標號。

     維護距離標號的方法是這樣的:當找增廣路過程中發現某點出發沒有允許弧時,將這個點的距離標號設爲由它出發的所有弧的終點的距離標號的最小值加一。這種維護距離標號的方法的正確性我就不證了。由於距離標號的存在,由於“怎麼走都是最短路”,所以就可以採用DFS找增廣路,用一個棧保存當前路徑的弧即可。當某個點的距離標號被改變時,棧中指向它的那條弧肯定已經不是允許弧了,所以就讓它出棧,並繼續用棧頂的弧的端點增廣。爲了使每次找增廣路的時間變成均攤O(V),還有一個重要的優化是對於每個點保存“當前弧”:初始時當前弧是鄰接表的第一條弧;在鄰接表中查找時從當前弧開始查找,找到了一條允許弧,就把這條弧設爲當前弧;改變距離標號時,把當前弧重新設爲鄰接表的第一條弧,還有一種在常數上有所優化的寫法是改變距離標號時把當前弧設爲那條提供了最小標號的弧。當前弧的寫法之所以正確就在於任何時候我們都能保證在鄰接表中當前弧的前面肯定不存在允許弧。

    還有一個常數優化是在每次找到路徑並增廣完畢之後不要將路徑中所有的頂點退棧,而是隻將瓶頸邊以及之後的邊退棧,這是借鑑了Dinic算法的思想。注意任何時候待增廣的“當前點”都應該是棧頂的點的終點。這的確只是一個常數優化,由於當前邊結構的存在,我們肯定可以在O(n)的時間內復原路徑中瓶頸邊之前的所有邊。

優化:

1.鄰接表優化:

如果頂點多的話,往往N^2存不下,這時候就要存邊:

存每條邊的出發點,終止點和價值,然後排序一下,再記錄每個出發點的位置。以後要調用從出發點出發的邊時候,只需要從記錄的位置開始找即可(其實可以用鏈表)。優點是時間加快空間節省,缺點是編程複雜度將變大,所以在題目允許的情況下,建議使用鄰接矩陣。

2.GAP優化:

如果一次重標號時,出現距離斷層,則可以證明ST無可行流,此時則可以直接退出算法。

3.當前弧優化:

爲了使每次找增廣路的時間變成均攤O(V),還有一個重要的優化是對於每個點保存“當前弧”:初始時當前弧是鄰接表的第一條弧;在鄰接表中查找時從當前弧開始查找,找到了一條允許弧,就把這條弧設爲當前弧;改變距離標號時,把當前弧重新設爲鄰接表的第一條弧。

另外,ISAP簡化的描述是:程序開始時用一個反向 BFS 初始化所有頂點的距離標號,之後從源點開始,進行如下三種操作:(1)當前頂點 i 爲終點時增廣 (2) 當前頂點有滿足 dist[i] = dist[j] + 1 的出弧時前進 (3) 當前頂點無滿足條件的出弧時重標號並回退一步。整個循環當源點 s 的距離標號 dist[s] >= n 時結束。對 i 點的重標號操作可概括爲 dist[i] = 1 + min{dist[j] : (i,j)屬於殘量網絡Gf}。

借用莊神的模板 http://www.zlinkin.com/?p=34 ,用它來過POJ 3469簡直無敵!比Dinic快好幾倍

  1. const int MAXN=20010; 
  2. const int MAXM=500010; 
  3. int n,m;//n爲點數 m爲邊數 
  4. int h[MAXN]; 
  5. int gap[MAXN]; 
  6. int p[MAXN],ecnt; 
  7. int source,sink; 
  8. struct edge{ 
  9.     int v;//邊的下一點 
  10.     int next;//下一條邊的編號 
  11.     int val;//邊權值 
  12. }e[MAXM]; 
  13.  
  14. inline void init(){memset(p,-1,sizeof(p));eid=0;} 
  15.   
  16. //有向 
  17. inline void insert1(int from,int to,int val){ 
  18.     e[ecnt].v=to; 
  19.     e[ecnt].val=val; 
  20.     e[ecnt].next=p[from]; 
  21.     p[from]=eid++; 
  22.   
  23.     swap(from,to); 
  24.   
  25.     e[ecnt].v=to; 
  26.     e[ecnt].val=0; 
  27.     e[ecnt].next=p[from]; 
  28.     p[from]=eid++; 
  29.   
  30. //無向 
  31. inline void insert2(int from,int to,int val){ 
  32.     e[ecnt].v=to; 
  33.     e[ecnt].val=val; 
  34.     e[ecnt].next=p[from]; 
  35.     p[from]=eid++; 
  36.   
  37.     swap(from,to); 
  38.   
  39.     e[ecnt].v=to; 
  40.     e[ecnt].val=val; 
  41.     e[ecnt].next=p[from]; 
  42.     p[from]=eid++; 
  43.   
  44.  
  45. inline int dfs(int pos,int cost){ 
  46.     if (pos==sink){ 
  47.         return cost; 
  48.     } 
  49.   
  50.     int j,minh=n-1,lv=cost,d; 
  51.   
  52.     for (j=p[pos];j!=-1;j=e[j].next){ 
  53.         int v=e[j].v,val=e[j].val; 
  54.         if(val>0){ 
  55.             if (h[v]+1==h[pos]){ 
  56.                 if (lv<e[j].val) d=lv; 
  57.                 else d=e[j].val; 
  58.   
  59.                 d=dfs(v,d); 
  60.                 e[j].val-=d; 
  61.                 e[j^1].val+=d; 
  62.                 lv-=d; 
  63.                 if (h[source]>=n) return cost-lv; 
  64.                 if (lv==0) break
  65.             } 
  66.   
  67.             if (h[v]<minh)   minh=h[v]; 
  68.         } 
  69.     } 
  70.   
  71.     if (lv==cost){ 
  72.         --gap[h[pos]]; 
  73.         if (gap[h[pos]]==0) h[source]=n; 
  74.         h[pos]=minh+1; 
  75.         ++gap[h[pos]]; 
  76.     } 
  77.   
  78.     return cost-lv; 
  79.   
  80.   
  81. int sap(int st,int ed){ 
  82.   
  83.     source=st; 
  84.     sink=ed; 
  85.     int ans=0; 
  86.     memset(gap,0,sizeof(gap)); 
  87.     memset(h,0,sizeof(h)); 
  88.   
  89.     gap[st]=n; 
  90.   
  91.     while (h[st]<n){ 
  92.         ans+=dfs(st,INT_MAX); 
  93.     } 
  94.   
  95.     return ans; 

對於EK算法與ISAP算法的區別:

     EK算法每次都要重新尋找增廣路,尋找過程只受殘餘網絡的影響,如果改變殘餘網絡,則增廣路的尋找也會隨之改變;SAP算法預處理出了增廣路的尋找大致路徑,若中途改變殘餘網絡,則此算法將重新進行。EK處理在運算過程中需要不斷加邊的最大流比SAP更有優勢。

發佈了158 篇原創文章 · 獲贊 23 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章