1. 程式人生 > >(通俗易懂小白入門)網路流最大流——EK演算法

(通俗易懂小白入門)網路流最大流——EK演算法

網路流

網路流是模仿水流解決生活中類似問題的一種方法策略,來看這麼一個問題,有一個自來水廠S,它要向目標T提供水量,從S出發有不確定數量和方向的水管,它可能直接到達T或者經過更多的節點的中轉,目前確定的是每條水管中水流的流向是確定的(單向),且每個水管單位時間內都有屬於自己的水流量的上限(超過會爆水管),問題是求終點T單位時間內獲得的最大水流量是多少?如下圖:

1. 首先,我們用正常的思路去解決這個問題,對於上圖的情況而言,我們可以先選擇一條水流的路線1->2->4,而且我們得知1->2水管水流量上限為40,而2->4水管水流量上限只有20,則這一條路徑通往終點T的水流最大值為20,而這麼走的話,1->2的水管中剩餘水流量則為40-20=20,2->4水管中的剩餘水量為20-20=0,注意:這裡水管剩餘流量為0後說明這條路徑就再也不能接通了,或者說它已經被完全佔用了,此時終點T單位時間獲得水量0+20=20

2. 然後我們繼續選擇一條不包含0流量的從S到T的路徑,比如1->2->3->4,此時1->2上水流量剩餘20,2->3剩餘30,3->4剩餘10,很顯然這條路徑上最終能為終點T提供的單位時間的水量由路徑上的最小剩餘流量10決定(就像是木桶的短板效應一樣),這麼走的話,1->2水管中剩餘的水量為40-20-10=10,2-3水管中剩餘的水量為30-10=20,3->4水管中剩餘的水量為10-10=0,它也被完全佔用了,此時終點T單位時間獲得水量為20+10=30

3. 我們繼續找還能找到一條從S流向T的路徑1->4,水流上限20,因為是直達的,所以路徑上最小剩餘流量就是它本身,則這麼走的話,1->4上的水流量全部耗盡,20-20=0,它也完全被佔用了,而終點T再一次獲得20水量,30+20=50,至此整張網路流圖再也找不到一條能從S到T的不包含0流量的通路,終點T單位時間獲得的最大水流也計算出來得到50

 

 但是,上述的推理只是碰巧數字比較合適讓我們輕而易舉得到了50這個看似正確的結果,實際上存在著缺陷,假如我第一次找的一條路徑並不是此時的最優解,並且這麼選使得我下一次選擇別的路徑的時候有的水管流量已經被佔完了,我想反悔怎麼辦

如下圖:如果我們第一次選擇的是1->2->3->4這條路徑(這很符合程式設計的觀念,計算機很容易就按照序號去查詢),那麼1->2水管剩餘流量為1-1=0,2->3水管剩餘流量為1-1=0,3->4水管的剩餘流量為1-1=0,至此我們發現所有的通路都被這三個0流量的水管阻斷了,而此時終點T獲得的單位水量只有0+1=1

按正常的人為的思路,此時我想要反悔之前選的那條路徑,同時選擇別的路徑,而下圖的1->2->4和1->3->4這兩條路徑的選擇才是這張網路流圖的最優選擇,終點T得到的最大水量為1+1=2,而2->3的這條水管我們放棄不用

上述反悔的過程在我們的思維看來確實可以(假裝)沒有走過2->3這條路,但是計算機並不能這麼去主動理解這種情況,它應該按並不是最優的情況去嘗試,當遇到可能更優的情況時更改之前的選擇,在我們設計程式的時候,如何能做到這個反悔的過程呢,關鍵來了:我們對於每次選擇的一條道路上的每個水管都要減去路徑上的最小殘量的同時,為這兩個相鄰的點新增一條反向弧,反向弧的數值加上最小殘量的數值,如上上圖那個並不是最優的走法,假設計算機確實第一次就選擇了這條路徑,那麼我們做出如下處理:1->2剩餘流量依舊為1-1=0,而同時新增一條2->1的反向弧,流量為0+1=1(反向弧也是一條通路,它的存在就像是為我們提供了一個返回的機會),2->3剩餘流量為1-1=0,新增3->2的反向弧,剩餘流量為0+1=1,同樣3->4的剩餘流量為1-1=0,而4->3的反向弧上的剩餘流量為0+1=1,所以本次選擇之後終點T依舊獲得了1的單位流量

接下來我們繼續選擇一條路徑,1->3->2->4(當然也只剩下這條了),此時我們如上述操作,為1->3剩餘流量,3->2剩餘流量,2->4剩餘流量都減去這條路徑上的最小殘量1,同時為它們兩兩之間的反向弧增加上這個最小殘量1(圖可能有點亂,但是原理是不變的,實線方向-,虛線方向+),至此,1->2=0,2->1=1,1->3=0,3->1=1,2->3=1,3->2=0,2->4=0,4->2=1,3->4=0,4->3=1,從S到T已經沒有一條額外的路徑不包含0流量了,此時T獲得最大單位水量2,並且通過這種建立反向弧的方式,我們對於從2到3再從3到2都走了一遍,這不就好像是模擬了一遍反悔的過程嗎(具體證明這裡不作詳述),接下來講解EK演算法

鋪墊了這麼多終於要講解EK演算法了

彆著急在講解EK之前我們需要介紹幾個網路流中最重要的概念,我們稱起點S,也就是水流的出發點為源點,將水流的終點T稱為匯點,而我們每一次找一條從源點到匯點的不包含0流量的路徑稱為增廣路,(增廣路顧名思義,如果能找到一條從S到T的增廣路,則到終點的水流量一定還可以增加至少1,所以就滿足了增廣這個要求了),其實求網路流最大流的問題的各個演算法都是在模擬一個找尋增廣路的過程,如果找不到了就說明此時終點獲得的水量將無法變得更大,答案就算出來了

EK演算法:從S點出發不斷找一條到T的增廣路的過程,我們通過BFS向周圍搜尋與S直接相連的剩餘流量不為0的節點(這個節點一定要是沒走過的,因為一條增廣路每個點肯定值出現了一次),將他們加入佇列,每次從佇列中取出一個元素繼續向周圍查詢,直到目標點為T點,且這一條道路上不包含流量為0的水管,則說明這是一條增廣路,為沿途的所有節點兩兩之間的剩餘流量減去該條增廣路的最小殘量,而同時為它們的反向弧加上最小殘量,不斷迴圈直到無法從S點到T找到一條增廣路為止

這裡推薦一題網路流最大流的模板題,POJ1273,題面講的是有n個水管,有m個點,源點為1,匯點為m,求匯點T單位時間的最大水流量,當然這題有個小坑,就是輸入會重複,如果1->2 40代表從1流向2有40流量,那可能會有多次1->2 40,1->2 30之類的,要累加成1->2 70

程式碼:

 

 1 #include<iostream>
 2 #include<stdio.h>
 3 #include<queue> 
 4 #include<string.h>
 5 using namespace std;
 6 
 7 const int N = 205;
 8 const int INF = 0x3f3f3f3f;
 9 int c[N][N];        //記錄i到j的剩餘流量 
10 int p[N];            //p[i]記錄流向i點的前驅節點
11 int v[N];            //記錄在一條增廣路中,流到i點時,此刻增廣路上殘餘量的最小值,直到i == m時就是整條增廣路上的殘餘量最小值 
12 int n, m;
13 
14 int min(int a, int b){
15     return a <= b ? a : b;
16 }
17 
18 void EK(){
19     //從1出發,不斷找可以到達m的增廣路
20     int ans = 0;
21     while(true){
22         //EK演算法的核心是通過bfs不斷查詢增廣路,同時建立反向弧
23         //每次迴圈都要對v陣列和p陣列進行清空,因為是意圖查詢一條新的增廣路了
24         memset(p, 0, sizeof(p));
25         memset(v, 0, sizeof(v)); 
26         queue<int> q;
27         q.push(1);
28         v[1] = INF;
29         //每次只找一條增廣路,同時修改c[i][j]的值 
30         while(!q.empty()){
31             int f = q.front();
32             q.pop();
33             for(int i = 1; i <= m; i++){
34                 if(v[i] == 0 && c[f][i] > 0){        //v[i]原本是記錄增廣路實時的殘量最小值,v[i]==0代表這個點還沒有走過,且從p到i的殘量大於0說明通路 
35                     v[i] = min(v[f], c[f][i]);        //實時更新v[i]的值,v[f]儲存1條增廣路中i點前所有水管殘量的最小值,v[i]為該條增廣路到i點為止,路徑上的最小殘量 
36                     p[i] = f;                        //p[i]實時儲存i點的前驅節點,這樣就當i==m時整條增廣路就被記錄下來 
37                     q.push(i);                        //將i點入隊 
38                 }
39             } 
40         }
41         if(v[m] == 0) break;                          //如果v[m]==0則代表找不到增廣路了(中途出現了c[i][j]==0的情況) 
42         ans += v[m];
43         int temp = m;
44         while(p[temp] != 0){                        //類似並查集的查操作,不斷查上一個元素且將剩餘殘量減去最小殘聯,反向弧增加最小殘量 
45             c[p[temp]][temp] -= v[m];
46             c[temp][p[temp]] += v[m];
47             temp = p[temp];
48         } 
49     } 
50     printf("%d\n", ans); 
51 }
52 
53 int main(){
54     while(scanf("%d%d", &n, &m) != EOF){
55         memset(c, 0, sizeof(c));
56         for(int i = 1; i <= n; i++){
57             int x, y, z;
58             scanf("%d%d%d", &x, &y, &z);
59             c[x][y] += z;                //初始時,從x流向y的剩餘流量就是輸入的值 
60         }
61         EK(); 
62     }
63     return 0;
64 } 

&n