1. 程式人生 > >圖論最大流(Edmond Karp演算法)

圖論最大流(Edmond Karp演算法)

Edmond Karp演算法的大概思想:

反覆尋找源點s到匯點t之間的增廣路徑,若有,找出增廣路徑上每一段[容量-流量]的最小值delta,若無,則結束。

在尋找增廣路徑時,可以用BFS來找,並且更新殘留網路的值(涉及到反向邊)。

而找到delta後,則使最大流值加上delta,更新為當前的最大流值。

(粗體表明需要掌握的概念)

關於反向邊:

以下摘至HDOJ的課件和網上的:

首先來看一下基本的網路流最大流模型。

有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點,通常規定為1號點。另一個點也很特殊,只進不出,叫做匯點,通常規定為n號點。每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[I,j]表示,流量則通常是f[I,j]。通常可以把這些邊想象成道路,流量就是這條道路的車流量,容量就是道路可承受的最大的車流量。很顯然的,流量<=容量。而對於每個不是源點和匯點的點來說,可以類比的想象成沒有儲存功能的貨物的中轉站,所有”進入”他們的流量和等於所有從他本身”出去”的流量。

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

比如這個圖。每條邊旁邊的數字表示它的容量。

1
下面我們來考慮如何求最大流。

首先,假如所有邊上的流量都沒有超過容量(不大於容量),那麼就把這一組流量,或者說,這個流,稱為一個可行流。一個最簡單的例子就是,零流,即所有的流量都是0的流。

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

這樣我們就得到了一個更大的流,他的流量是之前的流量+delta,而這條路就叫做增廣路。

我們不斷地從起點開始尋找增廣路,每次都對其進行增廣,直到源點和匯點不連通,也就是找不到增廣路為止。當找不到增廣路的時候,當前的流量就是最大流,這個結論非常重要。

尋找增廣路的時候我們可以簡單的從源點開始做bfs,並不斷修改這條路上的delta量,直到找到源點或者找不到增廣路。

這裡要先補充一點,在程式實現的時候,我們通常只是用一個c陣列來記錄容量,而不記錄流量,當流量+1的時候,我們可以通過容量-1來實現,以方便程式的實現。

先來看看BFS部分的程式碼(C/C++實現):

  1. // 用BFS來判斷從結點s到t的路徑上是否還有delta
  2. // 即判斷s,t之間是否還有增廣路徑,若有,返回1
  3. bool BFS(int s, int t)  
  4. {  
  5.     queue<int> que;  
  6.     memset(pre, -1, sizeof(pre));  
  7.     memset(vis, falsesizeof(vis));  
  8.     pre[s] = s;  
  9.     vis[s] = true;  
  10.     que.push(s);  
  11.     int p;  
  12.     while(!que.empty())  
  13.     {  
  14.         p = que.front();  
  15.         que.pop();  
  16.         for(int i=1; i<=M; ++i)  
  17.         {  
  18.             if(r[p][i]>0 && !vis[i])  
  19.             {  
  20.                 pre[i] = p;  
  21.                 vis[i] = true;  
  22.                 if(i == t)  // 存在增廣路徑
  23.                     returntrue;  
  24.                 que.push(i);  
  25.             }  
  26.         }  
  27.     }  
  28.     returnfalse;  
  29. }  


但事實上並沒有這麼簡單,上面所說的增廣路還不完整,比如說下面這個網路流模型。
2

我們第一次找到了1-2-3-4這條增廣路,這條路上的delta值顯然是1。於是我們修改後得到了下面這個流。(圖中的數字是容量)

3

這時候(1,2)和(3,4)邊上的流量都等於容量了,我們再也找不到其他的增廣路了,當前的流量是1。

但這個答案明顯不是最大流,因為我們可以同時走1-2-4和1-3-4,這樣可以得到流量為2的流。

那麼我們剛剛的演算法問題在哪裡呢?問題就在於我們沒有給程式一個”後悔”的機會,應該有一個不走(2-3-4)而改走(2-4)的機制。那麼如何解決這個問題呢?回溯搜尋嗎?那麼我們的效率就上升到指數級了。

而這個演算法神奇的利用了一個叫做反向邊的概念來解決這個問題。即每條邊(I,j)都有一條反向邊(j,i),反向邊也同樣有它的容量。

我們直接來看它是如何解決的:

在第一次找到增廣路之後,在把路上每一段的容量減少delta的同時,也把每一段上的反方向的容量增加delta。即在Dec(c[x,y],delta)的同時,inc(c[y,x],delta)

我們來看剛才的例子,在找到1-2-3-4這條增廣路之後,把容量修改成如下

4

這時再找增廣路的時候,就會找到1-3-2-4這條可增廣量,即delta值為1的可增廣路。將這條路增廣之後,得到了最大流2。

5

那麼,這麼做為什麼會是對的呢?我來通俗的解釋一下吧。

事實上,當我們第二次的增廣路走3-2這條反向邊的時候,就相當於把2-3這條正向邊已經是用了的流量給”退”了回去,不走2-3這條路,而改走從2點出發的其他的路也就是2-4。(有人問如果這裡沒有2-4怎麼辦,這時假如沒有2-4這條路的話,最終這條增廣路也不會存在,因為他根本不能走到匯點)同時本來在3-4上的流量由1-3-4這條路來”接管”。而最終2-3這條路正向流量1,反向流量1,等於沒有流量。

這就是這個演算法的精華部分,利用反向邊,使程式有了一個後悔和改正的機會。而這個演算法和我剛才給出的程式碼相比只多了一句話而已。

至此,最大流Edmond-Karp演算法介紹完畢。

Edmond Karp演算法具體實現(C/C++):

  1. /** 
  2.  * Edmond Karp 
  3.  * Max Flow 
  4.  * by Tanky Woo @ www.wutianqi.com 
  5.  */
  6. #include <iostream>
  7. #include <queue>
  8. #include <algorithm>
  9. usingnamespace std;  
  10. constint msize = 205;  
  11. int N, M;   // N--路徑數, M--結點數
  12. int r[msize][msize];  //
  13. int pre[msize];  // 記錄結點i的前向結點為pre[i]
  14. bool vis[msize]; // 記錄結點i是否已訪問
  15. // 用BFS來判斷從結點s到t的路徑上是否還有delta
  16. // 即判斷s,t之間是否還有增廣路徑,若有,返回1
  17. bool BFS(int s, int t)  
  18. {  
  19.     queue<int> que;  
  20.     memset(pre, -1, sizeof(pre));  
  21.     memset(vis, falsesizeof(vis));  
  22.     pre[s] = s;  
  23.     vis[s] = true;  
  24.     que.push(s);  
  25.     int p;  
  26.     while(!que.empty())  
  27.     {  
  28.         p = que.front();  
  29.         que.pop();  
  30.         for(int i=1; i<=M; ++i)  
  31.         {  
  32.             if(r[p][i]>0 && !vis[i])  
  33.             {  
  34.                 pre[i] = p;  
  35.                 vis[i] = true;  
  36.                 if(i == t)  // 存在增廣路徑
  37.                     returntrue;  
  38.                 que.push(i);  
  39.             }  
  40.         }  
  41.     }  
  42.     returnfalse;  
  43. }  
  44. int EK(int s, int t)  
  45. {  
  46.     int maxflow = 0, d;  
  47.     while(BFS(s, t))  
  48.     {  
  49.         d= INT_MAX;  
  50.         // 若有增廣路徑,則找出最小的delta
  51.         for(int i=t; i!=s; i=pre[i])  
  52.             d = min(d, r[pre[i]][i]);  
  53.         // 這裡是反向邊,看講解
  54.         for(int i=t; i!=s; i=pre[i])  
  55.         {  
  56.             r[pre[i]][i] -= d;  
  57.             r[i][pre[i]] += d;  
  58.         }  
  59.         maxflow += d;  
  60.     }  
  61.     return maxflow;  
  62. }  
  63. int main()  
  64. {  
  65.     while(cin >> N >> M)  
  66.     {  
  67.         memset(r, 0, sizeof(r));  
  68.         int s, e, c;  
  69.         for(int i=0; i<N; ++i)  
  70.         {  
  71.             cin >> s >> e >> c;  
  72.             r[s][e] += c;   // 有重邊時則加上c
  73.         }  
  74.         cout << EK(1, M) << endl;  
  75.     }  
  76.     return 0;  
  77. }