[转载]网络流笔记

http://www.csie.ntnu.edu.tw/~u91029/Flow.html#7

存档用...

s-t Flow

所謂伊人,在水一方。溯洄從之,道阻且長;溯游從之,宛在水中央。《詩經.蒹葭》

Flow Network

把一張圖想成是水流管線圖。圖上的邊想成是水管:邊的權重想成是水管的容量上限(容量下限預設為零),有向邊僅允許單向流動,無向邊得同時雙向流動。圖上的點想成是水管的接合點,並附有控制水流流向與流量的機器:點的權重想成是接合處的容量上限(容量下限預設為零),但是大家一般都不考慮點的權重。

每一條管線的流量數值與容量上限數值,以一斜線區隔,標記於圖上各條邊,方便觀看。水流流動時必須遵守各條管線的容量限制,不得有逾越容量限制之情事。流量數值與容量上限數值一定是正值或零,不得為負值。

在這張水流管線圖當中,水流流速是穩定的、是源源不絕的,有變化的只有水流流向與流量。因此,水流流動時,只要關心各條管線的方向限制和容量限制就可以了。

當一張圖專門用於水流流動時,則可稱之為 Flow Network ,中譯為「流網路」。

「流網路」只有容量資訊,沒有流量資訊。

s-t Flow

在圖上選定一個源點( source ,標記為 s )和一個匯點( sink ,標記為 t ),源點灌水,匯點泄水,並控制水流從源點流至匯點,中途不得滲漏、不得淤滯。

s-t Flow 以下簡稱為「流」。一個流便是由源點經管線至匯點的水流。一個流的流量,即是源點灌入的水流流量,同時也是匯點泄出的水流流量。

「流」只有流量資訊,沒有容量資訊。

Maximum s-t Flow

「最大流」。給定一張圖,以及給定一個源點與一個匯點,所有可能的 Flow 當中,流量最大者便是 Maximum Flow ,可能會有許多個。

在源點一口氣灌入大量的水,藉由調整各條管線的流量與流向,讓匯點泄出的流量最多。

Minimum s-t Flow

「最小流」。一滴水都不流,管線裡都沒水,就是 Min-Flow ,流量為零。大家應該都懂,所以就討論到這裡了。

圖例:不屬於流的玩意

水流流到無法流動的地方,水流淤滯而無法流至匯點,不能稱作流。

圖例:合流、分流、交流

水流可以在任何點上合流、分流、交流 ── 簡單地來說,就是每個點之中,流入與流出的水量要相等,至於要怎麼分合都無所謂。

圖例:產生迴圈的流

產生迴圈的流會佔據管線容量,令流量難以再增加,是一種浪費。

圖例:源點與匯點在一起的流

感情很好的兩個點,一般視作內地裡波濤洶湧,表面上流量為零。

多個源點變成一個源點、多個匯點變成一個匯點

先前都只討論一個源點和一個匯點的原因,其實是因為多個源點可以轉化成一個源點、多個匯點可以轉化成一個匯點。當圖上有多個源點時,就在圖上新增一個超級源點,連向這些源點,邊的容量都設定為無限大。如此一來,就可以只留下一個超級源點,並取消原本的源點了。匯點的道理亦同。

因此,在 s-t Flow 當中,多個源點和多個匯點可以改為一個源點和一個匯點,最後只討論一個源點和一個匯點就可以了。事情也變簡單多了。

點的容量變成邊的容量

先前提到大家一般不考慮點的容量,其實是因為點的容量可以轉化為邊的容量。把 P 點改成兩個點 Pin 和 Pout ,原先連到 P 點的邊變成連到 Pin ,由 P 點連出的邊變成由 Pout 連出, P 點的容量則由一條 Pin 到 Pout 的邊來取代之。

因此,在 s-t Flow 當中,點的容量可以改為邊的容量,最後只要考慮邊的容量就行了。只考慮邊的容量,事情也變簡單多了。

多重的邊變成單獨的邊

無向圖中,當兩點之間有多重的邊,就可以加總這些邊的容量限制,合併成單獨的邊;有向圖中,當一點到另一點有多重的邊,就可以加總這些邊的容量限制,合併成單獨的邊。

因此,在 s-t Flow 當中,多重的邊可以改為單獨的邊,最後只討論「無向圖:兩點間僅有一條邊、有向圖:一點到另一點僅有一條邊」就可以了。事情也變簡單多了。

來回水流變成單向水流

兩點之間,兩條方向相反的有向邊,等量減少來向與回向的水流,不會影響總流量,也不會違背規則。

因此,在 s-t Flow 當中,來回水流可以變成單向水流,最後只要從中選擇一條邊來流動就可以了。事情也變簡單多了。

無向邊變成有向邊

無向邊得同時雙向流動。一條無向邊可以改為兩條方向相反的有向邊,可是必須共用容量。

由於來回水流可以變成單向水流,所以上述兩條方向相反的有向邊,其實不必共用容量,宛如普通的有向邊。

因此,在 s-t Flow 當中,一條無向邊可以變成兩條方向相反的有向邊,最後只討論有向邊就可以了。事情也變簡單多了。

Max-Flow Min-Cut Theorem

一條涓流的流量與瓶頸

水從源點流至匯點,中途不會滲漏、不會淤滯。一條由源點流至匯點的小小涓流,其流動路徑當中,每一條邊的流量,都會是小小涓流的流量。

水流流動時要符合管線容量限制。一條由源點流至匯點的小小涓流,其流量的瓶頸,會是流動路徑當中,容量上限最低的一條邊。

一個流的總流量與瓶頸(一)

水從源點流至匯點,中途不會滲漏、不會淤滯。現在把圖上的點,依地理位置劃分作兩區,一區鄰近源點,另一區鄰近匯點 ── 一個從源點流至匯點的流,「由源點流至匯點的總流量」會等於「由源點區流入到匯點區的總流量」減去「由匯點區流回到源點區的總流量」。無論是哪一種分區方式,都有這種性質。

水流流動時要符合管線容量限制。一個從源點流至匯點的流,「由源點區流入到匯點區的總流量」會小於等於「由源點區橫跨到匯點區的管線總容量」。反方向亦同。

也就是說,一個從源點流至匯點的流,其總流量的瓶頸,會出現在「由源點區橫跨到匯點區的管線總容量」最低的一種分區方式。

一個流的總流量與瓶頸(二)

【讀者若不懂 s-t Cut ,請見本站文件「 Cut 」。】

方才的分兩區方式有點籠統,很難定義誰鄰近源點,誰鄰近匯點,因此資訊學家嘗試以 s-t Cut 來取代方才的說法,希望藉由 s-t Cut 把事情說得更嚴謹一些。然而事情也稍微變得複雜了。

水從源點流至匯點,中途不會滲漏、不會淤滯。現在把圖上的點,利用 s-t Cut 的概念劃分作兩側,一側包含源點,另一側包含匯點 ── 一個從源點流至匯點的流,「由源點流至匯點的總流量」會等於「由源點側流入到匯點側的總流量」減去「由匯點側流回到源點側的總流量」。無論是哪一種劃分方式,都有這種性質。

水流流動時要符合管線容量限制。一個從源點流至匯點的流,「由源點側流入到匯點側的總流量」會小於等於「由源點側橫跨到匯點側的管線總容量」。反方向亦同。

也就是說,一個從源點流至匯點的流,其總流量的瓶頸,會出現在「由源點側橫跨到匯點側的管線總容量」最低的一種分區方式。也就是容量的 Minimum s-t Cut 。

Max-Flow Min-Cut Theorem

「最大流最小割定理」其實就是談瓶頸。「網路流量的最大 s-t 流」等於「管線容量的最小 s-t 割」,管線若還有空間,就儘量增加流量,直到遇到瓶頸,瓶頸會出現在容量的最小 s-t 割。

打個比方來說,在一個裝水的塑膠袋底部戳洞,水就會流出來;戳越多洞,水就流出的越多。這表示水一旦有隙可乘,就一定會源源而來、滔滔而至,乃增加流量。水一旦無隙可乘,流量達到上限,此刻就是最大流了。

要找最大流,可以運用「最大流最小割定理」的概念,讓水流不停地鑽空隙流至匯點,當無法再找到空隙時,就是極限了,就是最大流了。

若只找最大流流量,則可以運用求最小 s-t 割的演算法,計算管線容量的最小 s-t 割的權重,即是最大 s-t 流流量。

Maximum s-t Flow: 
Augmenting Path Algorithm 
( Ford-Fulkerson Algorithm )

用途

給定一張圖,並給定源點、匯點,找出其中一個最大流。

Flow Decomposition

一個流是由許多條小小涓流逐漸聚集而成的。我們可以用一條一條的小小涓流,累積出最大流。

溯洄沖減
【註:此概念目前尚未有專有名詞】

然而有些涓流的路徑不理想,浪費了管線空間。有鑒於此,演算法作者設計了一個手法:溯洄沖減。流出第一條涓流之後,第二條涓流可以溯洄上一條流的部份路段,然後到達匯點。正流與逆流相互沖減的結果,使得相互交織的兩條涓流,成為兩條獨立的涓流。如此一來,就算涓流的路徑不理想,之後仍可靠溯洄沖減來調整路徑。

【註:涓流、溯洄、沖減這三個詞,是初次使用。我查字典後,覺得意義相近,而選用的。而且它們都是水字旁!】

溯洄沖減的時候,可以選擇水量多寡和路線位置,藉以調整成不同的流。

溯洄沖減的概念可以用於許多地方。這裡列出一些相關問題,供各位練習。

UVa 10806 10380

Residual Network

每次增加一條小小涓流,都要時時刻刻遵守各條管線的容量限制。管線要有剩餘空間,或者管線有溯洄沖減的機會,涓流才能流過管線。

一個便捷的整合方式是:管線的剩餘空間、再加上可供溯洄沖減的水量,稱作「剩餘容量 Residual Capacity 」;所有管線的剩餘容量,整體視作一張圖,稱作「剩餘網路 Residual Network 」。

如此一來,涓流要進行流動,只要參考剩餘網路,遵守各條管線的剩餘容量限制就可以了。

剩餘網路是一個高度抽象概念,為的是整合涓流流動的容量限制。實作程式碼時,可以直接建立一個剩餘網路的資料結構,隨時利用容量與剩餘容量來計算流量。

Augmenting Path

「擴充路徑」,剩餘網路上面一條由源點到匯點的路徑。換個角度想,把握管線的剩餘空間,把握溯洄沖減,找出一條由源點至匯點的小小涓流路徑,就是一條擴充路徑。

擴充路徑是一條可以擴充總流量的路徑,擴充的流量可多可少,一般來說流量越多越好,到達瓶頸最好,能較快達到最大流。

擴充路徑的長度範圍是 1 到 V-1 (長度為 0 表示源點與匯點重疊)。當一條擴充路徑超過 V-1 條邊,則會形成浪費管線容量的迴圈,此時應消除迴圈之後,才來做為擴充路徑。

演算法

不斷找擴充路徑,並擴充流量。直到沒有擴充路徑為止。

所有擴充路徑總合起來,就是最大流。
所有擴充路徑的流量總和,就是最大流流量。

擴充路徑要怎麼找都可以,
不一樣的擴充路徑有機會產生不一樣的最大流。
還找得到擴充路徑:
表示目前不是最大流,因為藉由擴充路徑,還可以再增加總流量。

找不到擴充路徑:
表示源點往匯點方向的一些關鍵管線已經沒有剩餘容量。沒有剩餘容量則表示:
甲、管線沒有剩餘空間(或根本沒有管線),也就是遭遇瓶頸。
乙、不能溯洄沖減。
根據最大流最小割定理,遭遇瓶頸時即是最大流。
【註:這部分的證明有漏洞。沒有考慮到形成迴圈的涓流。】

這個演算法的絕妙之處,是導入了溯洄沖減的機制,藉以調整流動路徑;然後把溯洄沖減併入了容量限制的思維,創立剩餘網路、擴充路徑等抽象概念;最後返回到最大流最小割定理,間接證明了溯洄沖減無論在什麼情況下,都能調整好流動路徑,找出最大流 ── 調校水流的本質,就是剩餘網路。

另外可以發現,在整個過程當中,所有管線的剩餘容量的總和是固定不變的 ── 只是源點往匯點方向的剩餘容量越來越少,匯點往源點方向的剩餘容量越來越多。整個過程可以看作是在調整剩餘網路的勢力走向 ── 每次以擴充路徑擴充流量後,正方向會減少對應的剩餘容量,逆方向會增加對應的剩餘容量,源點與匯點之間的勢力差距越來越大。

時間複雜度

找一條擴充路徑,等同一次 Graph Traversal 的時間。

最差情況下,擴充路徑流量通通都是 1 ,共找到 F 條, F 是最大流的流量。

圖的資料結構使用 adjacency matrix 為 O(V²F) ;圖的資料結構使用 adjacency lists 為 O((V+E)F) ,簡單寫成 O(EF) 。

如何記錄容量、流量、剩餘容量

為了實現溯洄沖減的概念,要是兩點之間只有單獨一條有向邊,就添配一條反向邊,讓雙向都有邊;要是兩點之間已經是兩條方向相反的有向邊,就不必更動;要是兩點之間是一條無向邊,則改成兩條方向相反的有向邊。

每一條邊的容量是定值;至於流量與剩餘容量,則有三種不同的記錄方式。第一種方式最直覺。第二種方式中庸。第三種方式最精簡,實作簡單,不過最後要計算最大流的時候會比較麻煩。

第一種記錄方式。先前提到,兩條方向相反的有向邊,可以只讓一條邊擁有水流,另一條邊則沒有水流。在此利用沒有水流的那條邊:兩點之間其中一個方向是正流量,是真正流動的方向;另一個方向則是虛設的負流量,用來增加剩餘容量以利溯洄沖減。

一開始還沒有水流的時候:
flow(i, j) = 0;
flow(j, i) = 0;

有一條涓流經過邊ij之後:
flow(i, j) += 流量;
flow(j, i) -= 流量;

有一條涓流經過邊ij,又有一條涓流溯洄沖減,經過邊ji之後:
flow(i, j) = flow(i, j) + 流量1 - 流量2;
flow(j, i) = flow(j, i) - 流量1 + 流量2;
最後可歸納得出,當一條涓流經過邊ij的時候:
flow(i, j) += 流量;
flow(j, i) = -flow(i, j);

真正的流量則是正流量:
true_flow(i, j) = max(flow(i, j), 0);
true_flow(j, i) = max(flow(j, i), 0);

剩餘容量以管線的容量上限和流量相減而得:
residue(i, j) = capacity(i, j) – flow(i, j);
residue(j, i) = capacity(j, i) – flow(j, i);
 
  1. typedef int Graph[10][10];      // adjacency matrix
  2. Graph C, F, R;                  // 容量上限、流量、剩餘容量
  3.  
  4. void one_stream_pass_ij()
  5. {
  6.     memcpy(R, C, sizeof(C));    // 最初每一條邊的剩餘容量等於容量上限
  7.     memset(F, 0, sizeof(F));    // 最初的流量為零
  8.  
  9.     F[i][j] = F[i][j] + 流量;
  10.     F[j][i] = -F[i][j];
  11.     R[i][j] = C[i][j] - F[i][j];
  12.     R[j][i] = C[j][i] - F[j][i];
  13. }

第二種記錄方式。只要有涓流經過了管線,就把涓流流量直接累加在管線流量。雖然這種記錄方式會讓流量違背容量限制,可是在剩餘容量正確的情況下,還是能找出最大流。

令邊ij是一條由i點到j點的邊。令邊ji是一條由j點到i的邊。

一開始還沒有水流的時候:
flow(i, j) = 0;
flow(j, i) = 0;

有一條涓流經過邊ij之後:
flow(i, j) += 流量;
flow(j, i) 保持不變;

有一條涓流經過邊ij,又有一條涓流溯洄沖減,經過邊ji之後:
flow(i, j) += 流量1;
flow(j, i) += 流量2;
最後可歸納得出,當一條涓流經過邊ij的時候:
flow(i, j) += 流量;
flow(j, i) 保持不變;

真正的流量是把雙向流量等量減少後而得(去掉溯洄沖減的部分):
true_flow(i, j) = flow(i, j) - min(flow(i, j), flow(j, i));
true_flow(j, i) = flow(j, i) - min(flow(i, j), flow(j, i));

剩餘容量以管線的容量上限和雙向流量計算而得:
residue(i, j) = capacity(i, j) – (flow(i, j) - flow(j, i));
residue(j, i) = capacity(j, i) – (flow(j, i) - flow(i, j));
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F, R;              // 容量上限、流量、剩餘容量
  3.  
  4. void one_stream_pass_ij()
  5. {
  6.     memcpy(R, C, sizeof(C));    // 最初每一條邊的剩餘容量等於容量上限
  7.     memset(F, 0, sizeof(F));    // 最初的流量為零
  8.  
  9.     F[i][j] = F[i][j] + 流量;
  10.     R[i][j] = C[i][j] - F[i][j] + F[j][i];
  11.     R[j][i] = C[j][i] - F[j][i] + F[i][j];
  12. }

第三種記錄方式。是第一種記錄方式的相反面向,主角改為剩餘容量,只要有涓流經過了管線,正方向剩餘容量會減少,反方向剩餘容量會增加。

令邊ij是一條由i點到j點的邊。令邊ji是一條由j點到i的邊。

一開始還沒有水流的時候:
residue(i, j) = capacity(i, j);
residue(j, i) = capacity(j, i);

有一條涓流經過邊ij之後:
residue(i, j) -= 流量;
residue(j, i) += 流量;

有一條涓流經過邊ij,又有一條涓流溯洄沖減,經過邊ji之後:
residue(i, j) = residue(i, j) - 流量1 + 流量2;
residue(j, i) = residue(j, i) + 流量1 - 流量2;
最後可歸納得出,當一條涓流經過邊ij的時候:
residue(i, j) -= 流量;
residue(j, i) += 流量;

真正的流量以管線的容量上限和剩餘流量相減而得,而且是正流量:
true_flow(i, j) = max(capacity(i, j) – residue(i, j), 0);
true_flow(j, i) = max(capacity(j, i) – residue(j, i), 0);
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F, R;              // 容量上限、流量、剩餘容量
  3.  
  4. void one_stream_pass_ij()
  5. {
  6.     memset(F, 0, sizeof(F));    // 最初的流量為零
  7.     memcpy(R, C, sizeof(C));    // 最初每一條邊的剩餘容量等於容量上限
  8.  
  9.     R[i][j] -= 流量;
  10.     R[j][i] += 流量;
  11.     F[i][j] = max(C[i][j] - R[i][j], 0);
  12.     F[j][i] = max(C[j][i] - R[j][i], 0);
  13. }

找出一個最大流+計算最大流的流量

 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F, R;              // 容量上限、流量、剩餘容量
  3.  
  4. int Ford_Fulkerson(int s, int t)    // 源點、匯點
  5. {
  6.     memset(F, 0, sizeof(F));    // 最初的流量為零
  7.  
  8.     while (存在一條擴充路徑:C[i][j] - F[i][j] > 0)
  9.         for (這條擴充路徑的每一條邊ij)
  10.         {
  11.             F[i][j] += 擴充路徑流量;
  12.             F[j][i] = -F[i][j];
  13.         }
  14. }
  15.  
  16. void Ford_Fulkerson(int s, int t)
  17. {
  18.     memset(F, 0, sizeof(F));    // 最初的流量為零
  19.  
  20.     while (存在一條擴充路徑:C[i][j] - F[i][j] + F[j][i] > 0)
  21.         for (這條擴充路徑的每一條邊ij)
  22.             F[i][j] += 擴充路徑流量;
  23. }
  24.  
  25. void Ford_Fulkerson(int s, int t)
  26. {
  27.     memcpy(R, C, sizeof(C));    // 最初每一條邊的剩餘容量等於容量上限
  28.  
  29.     while (存在一條擴充路徑:R[i][j] > 0)
  30.         for (這條擴充路徑的每一條邊ij)
  31.         {
  32.             R[i][j] -= 擴充路徑流量;
  33.             R[j][i] += 擴充路徑流量;
  34.         }
  35. }

Maximum s-t Flow: 
Shortest Augmenting Path Algorithm 
( Edmonds-Karp Algorithm )

演算法

Augmenting Path Algorithm 改良版。擴充路徑是源點到匯點的最短路徑(管線長度皆為 1 ),並且擴充流量至瓶頸。這種方式可以避免浪費管線空間,避免反覆地溯洄沖減,更快找到最大流。

不斷找最短擴充路徑,直到找不到為止,即得最大流。
最多找VE次就能達到最大流。

達到最大流,需要的最短擴充路徑數量。
( Edge Labeling with Shortest Distance )

剩餘網路的每一條邊,皆標記一個距離數值,數值大小是源點到該邊的最短距離。

每次以最短路徑擴充流量至瓶頸之後,一定有某些邊的距離數值會增加,並且沒有任何一條邊的距離數值會減少。換句話說,宏觀來看,距離數值與日俱增。

1. 最短擴充路徑上的正向邊:
 瓶頸前的邊:距離數值不變。
 瓶頸上的邊:距離數值增加。(正向邊斷掉,只剩下反向邊能通行。)
              (入口在彼端,距離數值至少增加一。)
       或者不變。(同時有多條最短路徑到達該邊,)
            (只是其中一條最短路徑斷了。)
 瓶頸後的邊:距離數值增加。(瓶頸斷掉,繞遠路。)
       或者不變。(同時有多條最短路徑到達該邊。)
2. 最短擴充路徑上的反向邊:距離數值增加或不變。但是不會減少。
3. 最短擴充路徑以外的邊:距離數值增加或不變。但是不會減少。
 (擴充流量而新增的反向邊,也不會減少源點到匯點的距離。)

甲、最差的情況下,每次擴充流量,只有一條邊的距離數值增加 1 。乙、圖上總共 E 條邊,每條邊的距離數值範圍為 0 到 V-1 。因此至多 VE 條最短擴充路徑,就能達到最大流。

達到最大流,需要的最短擴充路徑數量。
( Vertex Labeling with Shortest Distance )

剩餘網路的每一個點,皆標記一個距離數值,數值大小是源點到該點的最短距離。

每次以最短路徑擴充流量至瓶頸之後,最短路徑上的點的距離數值不見得會增加;源點到該點的所有最短路徑們通通截斷之後,距離數值才會增加。因此這種方式估計不出結果!

時間複雜度

以 BFS 尋找 O(VE) 條擴充路徑的時間。

圖的資料結構使用 adjacency matrix 為 O(V³E) ;圖的資料結構使用 adjacency lists 為 O((V+E)VE) ,簡單寫成 O(VE²) 。

找出一個最大流+計算最大流的流量

 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F, R;  // 容量上限、流量、剩餘容量
  3. bool visit[10]; // BFS經過的點
  4. int path[10];   // BFS tree
  5. int flow[10];   // 源點到各點的流量瓶頸
  6.  
  7. int BFS(int s, int t)   // 源點與匯點
  8. {
  9.     memset(visit, false, sizeof(visit));
  10.  
  11.     queue<int> Q;   // BFS queue
  12.     visit[s] = true;
  13.     path[s] = s;
  14.     flow[s] = 1e9;
  15.     Q.push(s);
  16.  
  17.     while (!Q.empty())
  18.     {
  19.         int i = Q.front(); Q.pop();
  20.         for (int j=0; j<10; ++j)
  21.             // 剩餘網路找擴充路徑
  22.             if (!visit[j] && R[i][j] > 0)
  23.             {
  24.                 visit[j] = true;
  25.                 path[j] = i;
  26.                 // 一邊找最短路徑,一邊計算流量瓶頸。
  27.                 flow[j] = min(flow[i], R[i][j]);
  28.                 Q.push(j);
  29.  
  30.                 if (j == t) return flow[t];
  31.             }
  32.     }
  33.     return 0;   // 找不到擴充路徑了,流量為零。
  34. }
  35.  
  36. int Edmonds_Karp(int s, int t)
  37. {
  38.     memset(F, 0, sizeof(F));
  39.     memcpy(R, C, sizeof(C));
  40.  
  41.     int f, df;  // 最大流的流量、擴充路徑的流量
  42.     for (f=0; df=BFS(s, t); f+=df)
  43.         // 更新擴充路徑上每一條邊的流量
  44.         for (int i=path[t], j=t; i!=j; i=path[j=i])
  45.         {
  46.             F[i][j] = F[i][j] + df;
  47.             F[j][i] = -F[i][j];
  48.             R[i][j] = C[i][j] - F[i][j];
  49.             R[j][i] = C[j][i] - F[j][i];
  50.         }
  51.     return f;
  52. }

計算最大流的流量

 
  1. int adj[10][10];        // adjacency matrix
  2. int q[10], *qf, *qb;    // BFS queue
  3. int p[10];              // BFS tree
  4.  
  5. int Edmonds_Karp(int s, int t)
  6. {
  7.     int f = 0;      // 最大流的流量
  8.     while (true)    // 不斷找擴充路徑直到找不到為止
  9.     {
  10.         // BFS找擴充路徑
  11.         memset(p, -1, sizeof(p));
  12.         qf = qb = q;
  13.         p[*qb++ = s] = s;
  14.         while (qf < qb && p[t] == -1)
  15.             for (int i = *qf++, j = 0; j < 10; ++j)
  16.                 if (p[j] == -1 && adj[i][j])
  17.                     p[*qb++ = j] = i;
  18.         if (p[t] == -1) break;
  19.  
  20.         // 更新擴充路徑上每一條邊的流量
  21.         int df = 1e9;
  22.         for (int i = p[t], j = t; i != j; i = p[j = i])
  23.             df = min(df, adj[i][j]);
  24.         for (int i = p[t], j = t; i != j; i = p[j = i])
  25.             adj[i][j] -= df, adj[j][i] += df;
  26.         f += df;
  27.     }
  28.     return f;
  29. }

UVa 820 10330 10779 563 10511 10983

Maximum s-t Flow: 
Blocking Flow Algorithm 
( Dinic's Algorithm )

抽刀斷水水更流。《李白.宣州謝朓樓餞別校書叔雲》

演算法

Shortest Augmenting Path Algorithm 改良版。一口氣找到一樣長的最短擴充路徑們。

重覆以下動作最多V-1次,直到無法擴充流量:
1. 計算residual network各點到源點(匯點)的最短距離。
2. 建立admissible network。
3. 尋找blocking flow,並擴充流量。

Admissible Network

剩餘網路上面,以源點(匯點)作為起點,計算源點(匯點)到每一點的最短距離。

剩餘網路上面,一條由源點往匯點方向的邊、兩端點最短距離相差一,稱作「容許邊 Admissible Edge 」。所有容許邊,整體視作一張圖,稱作「容許網路 Admissible Network 」。

容許網路是有向無環圖( DAG )、分層圖( Level Graph )。容許網路可以畫成一層一層的模樣,只有相鄰的層有邊。

容許網路就是剩餘網路的「最短路徑圖」。

容許網路上面,任意一條由源點到匯點的路徑,都是最短擴充路徑。藉由容許網路,可以迅速找到一樣長的最短擴充路徑們。

Blocking Flow

容許網路上面,一個源點到匯點的流,無法再擴充流量,稱作「阻塞流」,通常有許多種,也不必是最大流。

容許網路上面,逐次找到一樣長的最短擴充路徑們,並且每次都讓擴充的流量到達瓶頸,直到找不到為止;整體形成阻塞流。

演算法:找出一個阻塞流

容許網路上面,尋找最短擴充路徑,不必溯洄沖減。溯洄沖減會增加路徑長度,最後得到的不是最短擴充路徑。

由源點隨意往匯點走,若遇到死胡同,就重頭開始走,下次避免再走到死胡同。若順利走到匯點,就形成一條最短擴充路徑,並且擴充流量。

改由匯點隨意往源點走,就不會遇到死胡同。

一條最短擴充路徑,至少有一條邊是瓶頸。容許網路最多只有 E 條邊能作為瓶頸,所以一個阻塞流最多只有 E 條最短擴充路徑。

從源點走到匯點並擴充流量需時 O(V) ,最多有 O(E) 條最短擴充路徑,所以找出一個阻塞流的時間複雜度為 O(VE) 。

另外,使用「 Link-Cut Tree 」記錄容許網路,時間複雜度可以加速到 O(ElogV) 。

達到最大流,需要的阻塞流數量。
( Vertex Labeling with Shortest Distance )

前面章節利用 Vertex Labeling with Shortest Distance 估計不出結果,這裡卻可以。

剩餘網路上面,以阻塞流擴充流量,就斷絕了所有一樣長的最短擴充路徑。

容許網路上面,所有由源點到匯點的最短路徑們都阻塞了。剩餘網路上面,源點到匯點的最短距離會增加,下次的最短擴充路徑會更長;擴充流量而新增的反向邊,也不會減少源點到匯點的距離。

擴充路徑的長度範圍是 1 到 V-1 (長度為 0 表示源點與匯點重疊)。因此最多找 V-1 次阻塞流,就一定沒有擴充路徑了。

時間複雜度

找一個阻塞流需時 O(VE) ,最多找 O(V) 次,故總時間複雜度為 O(V²E) 。

找出一個最大流+計算最大流的流量

 
  1. const int V = 100, E = 1000;
  2.  
  3. int adj[V]; // adjacency lists,初始化為-1。
  4. struct Element {int b, r, next;} e[E*2];
  5. int en = 0;
  6.  
  7. void addedge(int a, int b, int c)
  8. {
  9.     e[en] = (Element){b, c, adj[a]}; adj[a] = en++;
  10.     e[en] = (Element){a, 0, adj[b]}; adj[b] = en++;
  11. }
  12.  
  13. int d[V];       // 最短距離
  14. bool visit[V];  // BFS/DFS visit record
  15. int q[V];       // queue
  16.  
  17. // 計算最短路徑,求出容許網路。
  18. int BFS(int s, int t)
  19. {
  20.     memset(d, 0x7f, sizeof(d));
  21.     memset(visit, false, sizeof(visit));
  22.  
  23.     int qn = 0;
  24.     d[s] = 0;
  25.     visit[s] = true;
  26.     q[qn++] = s;
  27.  
  28.     for (int qf=0; qf<qn; ++qf)
  29.     {
  30.         int a = q[qf];
  31.         for (int i = adj[a]; i != -1; i = e[i].next)
  32.         {
  33.             int b = e[i].b;
  34.             if (e[i].r > 0 && !visit[b])
  35.             {
  36.                 d[b] = d[a] + 1;
  37.                 visit[b] = true;
  38.                 q[qn++] = b;
  39.                 if (b == t) return d[t];
  40.             }
  41.         }
  42.     }
  43.     return V;
  44. }
  45.  
  46. // 求出一條最短擴充路徑,並擴充流量。
  47. int DFS(int a, int df, int s, int t)
  48. {
  49.     if (a == t) return df;
  50.  
  51.     if (visit[a]) return 0;
  52.     visit[a] = true;
  53.  
  54.     for (int i = adj[a]; i != -1; i = e[i].next)
  55.     {
  56.         int b = e[i].b;
  57.         if (e[i].r > 0 && d[a] + 1 == d[b])
  58.         {
  59.             int f = DFS(b, min(df, e[i].r), s, t);
  60.             if (f)
  61.             {
  62.                 e[i].r -= f;
  63.                 e[i^1].r += f;
  64.                 return f;
  65.             }
  66.         }
  67.     }
  68.     return 0;
  69. }
  70.  
  71. int dinic(int s, int t)
  72. {
  73.     int flow = 0;
  74.     while (BFS(s, t) < V)
  75.         while (true)
  76.         {
  77.             memset(visit, false, sizeof(visit));
  78.             int f = DFS(s, 1e9, s, t);
  79.             if (!f) break;
  80.             flow += f;
  81.         }
  82.     return flow;
  83. }

UVa 10546

延伸閱讀: MPM Algorithm

http://www.cs.cornell.edu/Courses/cs4820/2013sp/handouts/DinicMPM.pdf

容許網路上面,定義一個節點的容量是 min( 所有出邊總和 , 所有入邊總和 ) ,容量最小的節點即是瓶頸。

尋找阻塞流,不斷找到擴充路徑經過瓶頸(切兩段,先往源點找、再往匯點找),使用 Binary Heap 找瓶頸為 O(V²logV) ;使用 Fibonacci Heap 找瓶頸為 O(V²) 。找 V 次阻塞流為 O(V³) 。

Maximum s-t Flow: 
Capacity Scaling Algorithm

演算法

容量視作二進位數字,從最高數量級開始,每回合添加一個位數,並且擴充流量。

重複以下步驟logC回合:
1. 每條邊容量翻倍:流量隨著翻倍。
2. 每條邊容量加零或加一。
3. 尋找擴充路徑(或擴充流),填滿多出的容量,達到最大流。

時間複雜度

甲、總共 logC 回合。 C 是最大的管線容量。

乙、每回合開始之前,源點到匯點的剩餘容量已經填滿。每回合當中,添加到圖上各條邊的容量只有 0 或 1 ,剩餘容量頂多增加 E ,流量頂多擴充 E 。換句話說,每回合至多 E 條擴充路徑,就能達到最大流。

丙、找一條擴充路徑,等同一次 Graph Traversal 的時間。

圖的資料結構使用 adjacency matrix 為 O(V²ElogC) ;圖的資料結構使用 adjacency lists 為 O((V+E)ElogC) ,簡單寫成 O(E²logC) 。

計算最大流的流量

 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F, Cp; // 容量上限、流量、逐步增加精度的容量上限
  3.  
  4. int capacity_scaling(int s, int t)
  5. {
  6.     memset(F, 0, sizeof(F));
  7.     memset(Cp, 0, sizeof(Cp));
  8.  
  9.     int f = 0;  // 總流量
  10.  
  11.     // int有32個bit。
  12.     // 最高位的第32bit用以區分正負值,第31bit才是正整數的最高位。
  13.     // 第1bit向左位移30位到第31bit,就是正整數的最高位。
  14.     for (int b=1<<30; b; b>>=1)
  15.     {
  16.         for (int i=0; i<10; ++i)
  17.             for (int j=0; j<10; ++j)
  18.             {
  19.                 // 容量添加一個位數
  20.                 Cp[i][j] <<= 1;
  21.                 Cp[i][j] += !!(C[i][j] & b);
  22.  
  23.                 // 流量隨著翻倍
  24.                 F[i][j] <<= 1;
  25.             }
  26.  
  27.         f <<= 1;
  28.         f += Ford_Fulkerson(s, t, F, Cp);   // 計算最大流
  29.     }
  30.     return f;
  31. }
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, R;                 // 容量上限、剩餘容量
  3.  
  4. void capacity_scaling(int s, int t)
  5. {
  6.     memcpy(R, 0, sizeof(R));
  7.  
  8.     for (int b=1<<30; b; b>>=1)
  9.     {
  10.         for (int i=0; i<10; ++i)
  11.             for (int j=0; j<10; ++j)
  12.                 (R[i][j] <<= 1) += !!(C[i][j] & b);
  13.  
  14.         Ford_Fulkerson(s, t, R);    // 計算最大流,調整剩餘網路。
  15.     }
  16. }

Maximum s-t Flow: 
Preflow, Push, Relabel

壹、 push

想像一下:於源點放入足夠水量,然後用力推擠源點,就像針筒注射、發射水槍一樣,讓源點的水一股作氣鑽過整個流網路,最後從匯點噴出水流。

受限於流網路的管線容量瓶頸,水流流量是有上限的。水鑽過流網路的路線,就是一個最大流。匯點噴出的水流流量,就是最大流的流量。

然而電腦程式無法直接實現「一股作氣鑽過整個流網路」,電腦程式只能一個一個算、一步一步算,所以我們只好一個一個點慢慢推進:首先推進源點的水到其它中繼點,再繼續推進中繼點的水到其他中繼點,一個接著一個點慢慢地推進到匯點。

貳、 excess 和 overflowing

為了實現「一個接著一個點慢慢地推進」的想法,便定義圖上每個點都可以儲存水,成了「含水點」,其儲存水量稱作「含水量」。水被推到點上,得以暫時儲存在點上,以後隨時可以繼續推進。

建立含水點、含水量的系統之後,推進順序也無所謂了,因為水一直存在、不會消失,就算推歪方向,也可以往回推,回復原始狀態。

以「含水點」、「含水量」,設計一套找出最大流的方法:
子、先將源點的含水量設定成無限大。
丑、推進源點的水到圖上其他點,慢慢推向匯點,推進順序可隨意。
寅、多餘的水量,會受限於管線容量瓶頸,而留在源點和中繼點上。
卯、最後能夠推進到匯點的水量,就是最大流的流量。

參、 residual capacity

推進的同時也記錄管線流量,便可以知道水流流動的情形。管線流量與剩餘容量的概念請參考前面的 Augmenting Path Algorithm 章節。

推進一個點的水,甲、要注意點的含水量,乙、注意管線的剩餘容量,丙、盡量填滿管線,營造出針筒注射、發射水槍的效果。

 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F;                 // 容量上限、流量
  3. int e[10];                  // 各個點的含水量
  4.  
  5. void push(int i, int j)     // 推進i點的水到j點
  6. {
  7.     // 點上要有足夠水量,且流量不得超過剩餘容量。
  8.     int f = min(e[i], C[i][j] - F[i][j]);
  9.     e[i] -= f; e[j] += f;
  10.     F[i][j] += f; F[j][i] -= f;
  11. }

肆、 admissible edge

「一個接著一個點慢慢地推進到匯點」,要確保各點的水是確實地推向匯點、聚集在匯點,避免漫無目的來回推進,避免環狀推進、不斷繞圈圈。因此導入了「水往低處流」的想法。

以「水往低處流」來設計方法、解決問題:
子、推向匯點:源點高、匯點低、其他點排好適當的高低順序。
丑、由源點漫溢:源點是最高點。
寅、朝匯點聚集:匯點是最低點。
卯、避免繞圈圈:不能推水到一樣高的點。只能推水到更低的鄰點。

伍、 height label

為了實現「水往低處流」的想法,便定義圖上每個點都有一個「高度值」,並排好高低順序,由高往低推進、由源點向匯點推進。

高低順序有兩種安排方式:甲、反轉所有邊之後,以匯點為起點的最短路徑長度,作為高度值。乙、以源點為起點的最短路徑長度,取負值(可再加常數變成正值),作為高度值。

採用甲有個好處,因為推進規則可以改成:推進一個點的水,只能到、也只需要到「比此點剛好低一層」的鄰點。如此可以讓推進規則變得單純、容易實作,也依舊保持著水往低處流的原則。

排好高低次序,以及改變推進規則後,會出現新麻煩:甲、匯點不是最低點。乙、源點的水可能會推不出去。丙、現在要是推歪方向,就不能往回推了。丁、朝向匯點的管線容量不足時,一個點將會水洩不通。必須尋找其他管線,才能流向匯點。

以下將一一解決這些問題。

陸、 source and target

無論是哪一種高低順序安排方式,都不能保證源點最高、匯點最低。採用甲,可以把源點的高度另設為 V-1 ,源點就一定比圖上其他點高;匯點的高度維持為零,匯點就一定比圖上其他點低。 V 是圖上的點數。

 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F;                 // 容量上限、流量
  3. int d[10];                  // 高度
  4.  
  5. void set_height()
  6. {
  7.     shortest_path();
  8.     d[s] = V-1;
  9. }
  10.  
  11. // Dijkstra's Algorithm
  12. void shortest_path()
  13. {
  14.     bool v[10];
  15.     memset(d, 1, sizeof(d));
  16.     memset(v, 0, sizeof(v));
  17.     d[t] = 0;
  18.     for (int k=0; k<10; ++k)
  19.     {
  20.         int j, min = 1e9;
  21.         for (int i=0; i<10; ++i)
  22.             if (!v[i] && d[i] < min)
  23.                 min = d[j = i];
  24.  
  25.         v[j] = true;
  26.         for (int i=0; i<10; ++i)
  27.             if (!v[i] && C[i][j])
  28.                 d[i] = min(d[i], d[j] + 1);
  29.     }
  30. }

柒、 preflow

源點的高度設為 V-1 ,推進又只能到「比此點剛好低一層」的鄰點,這造成一開始的時候,可能無法推進源點的水到所有鄰點,或說源點的水可能無法流到所有鄰點。

為了解決這種狀況,一開始的時候,就預先推進源點的水到所有鄰點,稱作「預流」。

 
  1. int e[10];  // 含水點的含水量
  2.  
  3. void preflow(int s, int t)
  4. {
  5.     for (int j=0; j<10; ++j)
  6.         if (C[s][j] && j != s)
  7.         {
  8.             e[j] = C[s][j];
  9.             F[s][j] = e[j]; F[j][s] = -e[j];
  10.         }
  11. }

捌、 relabel

另外又追加了「抬高」的想法:當一個含水點水洩不通,就稍微抬高它,讓水可以流過其他管線,到達其他鄰點;甚至可以抬高到往回流,矯正水流流向。

現在不必預先設定每個點的高低次序了,只需固定源點和匯點的高度,讓各個點抬高後總是比源點低、比匯點高即可。想要推動一個含水點的水,就抬高此點;抬高一個含水點,就可以推動此點的水。

以「水往低處流」和「抬高含水點」來設計方法、解決問題:
子、推向匯點:抬高一個點到比匯點還高,但不能抬高匯點。
丑、由源點漫溢:源點是最高點,高度設為V-1,並預流。
寅、朝匯點聚集:匯點是最低點,高度設為0。
卯、避免繞圈圈:不能推水到一樣高的點。只能推水到剛好低一層的鄰點。
辰、避免水洩不通:抬高一個點比鄰近的點還高,以紓解積水。
巳、矯正流向:抬高一個點比來源的點還高,以豆退嚕。

玖、 saturating push

如果一個含水點有許多鄰點,就先抬高含水點稍高於最低的鄰點,並推水到最低的鄰點,並盡量令管線滿溢;如果還有剩水,就再抬高含水點稍高於次低的鄰點,並推水到次高的鄰點,依此類推。以匯點的角度來看,朝向匯點的各條管線會陸陸續續流過水流並且滿溢,最後就會得到最大流。各位可以觀察看看。

當一個含水點水洩不通,表示下游已經遭遇瓶頸、或說下游管線已經滿溢(甚至根本沒有管線)、或說沒有剩餘容量、或說無法再推更多水到匯點、或說有多餘的水流不到匯點。

當一個含水點水洩不通,就抬高含水點,讓水往回流,矯正流向,替多餘的水,尋求其他出路,流到匯點。相反的,當一個含水點河涸海乾時,就沒有必要抬高此點,找自己麻煩。

 
  1. void relabel(int i) // 抬高i點,直到能夠推水。
  2. {
  3.     int mind = 1e9;
  4.     for (int j=0; j<10; ++j)
  5.         if (C[i][j] - F[i][j] > 0)
  6.             mind = min(mind, d[j]);
  7.     d[i] = mind + 1;
  8. }

拾、 retreat (沒有專用術語,故自行命名)

當水量過剩,又不斷抬高圖上每個點,最後圖上每個點都會比源點還高,所有剩水都會回歸源點。這剛好可以作為演算法的閉幕。此時圖上的水流流動情形就是最大流。

以「水往低處流」和「抬高含水點」來設計方法、解決問題:
子、推向匯點:抬高一個點到比匯點還高,但不能抬高匯點。
丑、回歸源點:抬高一個點到比源點還高,但不能抬高源點。
寅、由源點漫溢:源點是最高點,高度設為V-1。
卯、朝匯點聚集:匯點是最低點,高度設為0。
辰、避免繞圈圈:不能推水到一樣高的點。只能推水到剛好低一層的鄰點。
巳、避免水洩不通:抬高一個點比鄰近的點還高,以紓解積水。
午、矯正流向:抬高一個點比來源的點還高,以豆退嚕。

小結

欲讓水「一股作氣鑽過整個流網路」計算最大流,電腦卻只能「逐點推進」,只好製作一些中繼的「含水點」,加入「水往低處流」的概念,以匯點為起點的最短路徑長度設定「高低次序」,迫使水流向匯點。但是卻導致「源點不是最高點」、「源點無法推水」、「無法矯正流向」、「水洩不通」等諸多問題。於是又出現了「設定源點匯點高度」、「預流」、「抬高」的想法,解決了上述問題,從此亦不再需要安排「高低次序」。至於無法推到匯點的剩水,恰可藉由「抬高」而回歸源點,演算法完美結束。

接下來開始詳細列出演算法內容。

Preflow

推動源點的水到所有鄰點。
(源點可以不必設定水量,不影響結果。)
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F;                 // 容量上限、流量
  3. int e[10];                  // 含水點的含水量
  4.  
  5. void preflow(int s)
  6. {
  7.     for (int j=0; j<10; ++j)
  8.         if (C[s][j] && j != s)
  9.         {
  10.             e[j] = C[s][j];
  11.             F[s][j] = e[j]; F[j][s] = -e[j];
  12.         }
  13. }
 
  1. int adj[10][10];    // adjacency matrix
  2. int e[10];          // 含水點的含水量
  3.  
  4. void preflow(int s)
  5. {
  6.     for (int j=0; j<10; ++j)
  7.         if (C[s][j] && j != s)
  8.         {
  9.             adj[j][s] += (e[j] = adj[s][j]);
  10.             adj[s][j] = 0;
  11.         }
  12. }

Push

給定一個含水點和一個與其相鄰的點,推水過去。
以下是允許進行Push的條件,確定符合後才得進行Push:
壹、含水點不是源點和匯點。(源點已預流,匯點收集水。)
貳、含水點仍有水。
參、含水點到其鄰點的邊仍有剩餘容量。
肆、鄰點是比含水點低一層的點。
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F;                 // 容量上限、流量
  3. int e[10];                  // 高度、含水點的含水量
  4.  
  5. void push(int i, int j)
  6. {
  7.     // 水量要足,且不得超過剩餘容量。
  8.     int f = min(e[i], C[i][j] - F[i][j]);
  9.     e[i] -= f; e[j] += f;
  10.     F[i][j] += f; F[j][i] -= f;
  11. }
 
  1. int adj[10][10];    // adjacency matrix
  2. int e[10];          // 高度、含水點的含水量
  3.  
  4. void push(int i, int j)
  5. {
  6.     // 水量要足,且不得超過剩餘容量。
  7.     int f = min(e[i], adj[i][j]);
  8.     e[i] -= f; e[j] += f;
  9.     adj[i][j] -= f; adj[j][i] += f;
  10. }

Relabel

給定一個含水點,抬高此含水點。
以下是允許進行Relabel的條件,確定符合後才得進行Relabel:
壹、含水點不是源點和匯點。(請參考Push,沒必要推動就沒必要抬高。)
貳、含水點仍有水。
參、含水點水洩不通。
  含水點藉由有剩餘容量的邊,所連到的鄰點,這些鄰點的高度都小於等於含水點。
  (當含水點仍找得到低一層的鄰點可以推水過去,就不能抬高。)
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F;                 // 容量上限、流量
  3. int d[10];                  // 高度
  4.  
  5. void relabel(int i)
  6. {
  7.     int mind = 1e9;
  8.     for (int j=0; j<10; ++j)
  9.         if (C[i][j] - F[i][j] > 0)
  10.             mind = min(mind, d[j]);
  11.     d[i] = mind + 1;
  12. }
 
  1. int adj[10][10];    // adjacency matrix
  2. int d[10];          // 高度
  3.  
  4. void relabel(int i)
  5. {
  6.     for (int j=0; j<10; ++j)
  7.         if (adj[i][j])
  8.             d[i] = min(d[i], d[j]);
  9.     d[i]++;
  10. }

演算法

1. 把圖上每個點的高度設為零。
   (或者是以匯點作為起點的最短路徑長度作為高度,不影響結果。)
2. 設定起點的高度是V-1(以上),V為圖上點數。
3. preflow。
4. 圖上各點不斷push或relabel,次序隨意,直到無法進行為止。
   (或者說,直到圖上除源點匯點以外的所有點都沒水為止。)
5. 匯點所收集的水量,即是最大流的流量。
   多餘的水流回源點後,源點所流出的水量,即是最大流的流量。
   圖上每條邊的水流,總合起來就是最大流。

經由複雜的推導,總算歸納出單純的演算法 ── 僅以三種「點對鄰點之間」的動作 preflow 、 push 、 relabel ,即可求得最大流。十分精采!

時間複雜度

我們針對 preflow 、 push 、 relabel 的次數下手。

preflow :總共一次, O(V) 。

relabel :設定匯點的高度為 0 ,源點的高度為 V-1 。最差的情況下,除源點匯點,最高的點升到了 2V-3 、最低的點升到了 V ,流不到匯點的水都回歸匯點了,如下圖所示。利用等差級數梯形公式,得知 relabel 總共最多 O(V²) 次。

圖的資料結構為 adjacency matrix ,一次 relabel 需時 O(V) ,全部的 relabel 需時 O(V³) ;圖的資料結構為 adjacency lists ,圖上各點各做一次 relabel 需時 O(E) ,全部的 relabel 需時 O(VE) 。

saturating push :對一條邊而言,兩個端點高度逐漸升高,高度範圍為 0 到 2V-3 ,高度相差一時才能推動,一種高度頂多推動一次,所以一條邊的 saturating push 總共最多 O(V) 次。

圖的資料結構為 adjacency lists ,一次 push 需時 O(1) ,一條邊的 saturating push 總共最多 O(V) 次,圖上總共 E 條邊,全部的 saturating push 需時 O(VE) 。

non-saturating push :同上,一種高度最多推動 V 次。由於其端點的 V 個鄰點升高時會補水給端點、其端點最多補水 V 次。全部的 non-saturating push 需時 O(V²E) 。

歸納上述四項,整個演算法的時間複雜度為 O(V²E) ,受限於 non-saturating push 的次數。

計算最大流的流量

實作時我們建立一個 queue 來儲存含水點。

 
  1. int adj[10][10];        // adjacency matrix
  2. int d[10], e[10];       // 高度、含水點的含水量
  3. int q[150], *qf, *qb;   // 存放圖上所有除源點和匯點外的含水點
  4.  
  5. void preflow(int s)
  6. {
  7.     for (int j=0; j<10; ++j)
  8.         if (adj[s][j] && j != s)
  9.         {
  10.             adj[j][s] += (e[j] = adj[s][j]);
  11.             adj[s][j] = 0;
  12.             if (j != s && j != t) *qb++ = j;
  13.         }
  14. }
  15.  
  16. void push(int i, int j)
  17. {
  18.     // 水量要足,且不得超過剩餘容量。
  19.     int f = min(e[i], adj[i][j]);
  20.     e[i] -= f; e[j] += f;
  21.     adj[i][j] -= f; adj[j][i] += f;
  22. }
  23.  
  24. void relabel(int i)
  25. {
  26.     for (int j=0; j<10; ++j)
  27.         if (adj[i][j])
  28.             d[i] = min(d[i], d[j]);
  29.     d[i]++;
  30. }
  31.  
  32. int preflow_push_relabel(int s, int t)
  33. {
  34.     memset(d, 0, sizeof(d));
  35.     memset(e, 0, sizeof(e));
  36.     qf = qb = q;
  37.     d[s] = 10 - 1;  // 設定源點高度,避免水太早灌回源點。
  38.     preflow(s);
  39.  
  40.     while (qf < qb) // 除源點匯點外還有含水點就繼續
  41.     {
  42.         int i = *qf++;
  43.         relabel(i); // 不一定能成功抬高此點,但無妨。
  44.  
  45.         for (int j=0; j<10; ++j)
  46.             if (d[i] == d[j] + 1 && adj[i][j])
  47.             {
  48.                 if (!e[j] && j!=s && j!=t) *qb++ = j;
  49.                 push(i, j);
  50.                 if (!e[i]) break;
  51.             }
  52.  
  53.         if (e[i]) *qb++ = i;
  54.     }
  55.     return e[t];
  56. }

Maximum s-t Flow: 
Discharge

想法

順著高低順序推水是我們的初衷。累積足夠水量後,就慢慢往前推動,不要每次都一口氣推水到最低處,便可以減少 push 的次數。

Discharge

給定一個含水點,不斷使用Push和Relabel把水排掉,直到沒水。
以下是允許進行Discharge的條件,確定符合後才得進行Discharge:
壹、含水點不是源點和匯點。
貳、含水點仍有水。
 
  1. typedef int Graph[10][10];  // adjacency matrix
  2. Graph C, F;                 // 容量上限、流量
  3. int d[10], e[10];           // 高度、含水點的含水量
  4.  
  5. void discharge(int i)
  6. {
  7.     while (e[i])    // 還有水就繼續排水
  8.     {
  9.         for (int j=0; j<10; ++j)
  10.             if (d[i] == d[j] + 1 && C[i][j] - F[i][j])
  11.             {
  12.                 push(i, j);
  13.                 if (!e[i]) return;  // 沒有水就結束
  14.             }
  15.         relabel(i); // 肯定可以抬高
  16.     }
  17. }

演算法

1. preflow。
2. 不斷找符合條件的含水點實施discharge,
   直到所有含水點(除源點匯點)都沒水為止。

   條件:其他含水點(除源點匯點)的水,
   無法沿著目前的容許網路流入此含水點。

【待確認】

時間複雜度

我們針對 preflow 、 push 、 relabel 的次數下手。 preflow 、 relabel 、 saturating push 的時間複雜度都與先前章節相同。此處只分析 non-saturating push 的時間複雜度。

【待補文字】

由均攤分析可知 non-saturating push 總共 O(V³) 次。整個演算法的時間複雜度為 O(V³) 。

演算法:一直找最高的含水點 discharge 
( Highest-Label Preflow-Push Algorithm )

1. preflow。
2. 一直找最高的含水點進行discharge(不包括源點匯點),
   直到圖上無含水點(不包括源點匯點),

演算法:含水點 discharge 後,若有升高則挪動順序至首
( Relabel-to-front Algorithm )

1. 建立一個list,裡面包含所有點(但不包括源點匯點)。
   註:此list從頭到尾一直都是當下容許網路的拓撲排序。
2. preflow。
3. 按照list順序讀取各點
   甲、如果不是含水點,就繼續下一個點,
   乙、如果是含水點,就discharge,並且於discharge完之後
      a. 如果剛才的discharge有抬高此點(有relabel),
         就把此點移到list開頭,並重新由list開頭讀取。
      b. 如果剛才的discharge沒有抬高此點(沒有relabel),
         就繼續下一個點。

演算法:含水點以 FIFO 順序 discharge 
( FIFO Preflow-Push Algorithm )

1. 建立一個queue,裡面只放含水點(但不包括源點匯點),含水點不會重複。
2. preflow,順便把含水點都放入queue。
3. 不斷從queue中取出點進行discharge,直到queue中無點。
   若discharge時產生了queue裡面沒有的含水點,就放入queue。

Maximum s-t Flow: 
Improved Shortest Augmenting Path Algorithm

註記

此演算法沒有廣為人知的正式名稱。

此演算法為 Ahuja 與 Orlin 於 1991 年發表,論文名稱是 Distance-directed augmenting path algorithms for maximum flow and parametric maximum flow problems 。該篇論文中同時發表了兩個最大流演算法,因此若直接稱呼 Distance-directed Augmenting Path Algorithm ,會無法準確指出是哪一個演算法。

Ahuja 與 Orlin 後來出版一本網路流的書籍,書上描述此演算法為「 Shortest Augmenting Path Algorithm 改良版」,但是仍未給予一個適切的名稱。

演算法

Preflow-Push Algorithm 的加強版。以 DFS 的順序沿著容許邊行走,當遇到死胡同,就 relabel ,並且回溯,尋找其他可以到達匯點的路徑。

與 Preflow-Push Algorithm 不同的地方,在於此演算法是找到匯點之後,才沿著擴充路徑來擴充流量,而不是逐點推水。

時間複雜度仍是 O(V²E) 。

 
  1. const int V = 100;
  2. int C[V][V], R[V][V];
  3. int h[V], cnt[V+1];
  4. int p[V], pf[V];
  5.  
  6. int ISAP(int s, int t)
  7. {
  8.     memcpy(R, C, sizeof(C));
  9.     memset(h, 0, sizeof(h));
  10.     memset(cnt, 0, sizeof(cnt));
  11.     cnt[0] = V;
  12.     pf[s] = 1e9;
  13.     p[s] = s;
  14.  
  15.     int flow = 0, i = s, j = 0, f = 1e9;
  16.     while (h[s] < V)
  17.     {
  18.         // 尋找一條容許邊
  19.         for (j=0; j<V; ++j)
  20.             if (R[i][j] > 0 && h[i] == h[j] + 1)
  21.                 break;
  22.  
  23.         // 沿著容許邊前進,到達匯點,擴充流量。
  24.         if (j == t)
  25.         {
  26.             f = min(f, R[i][j]);
  27.             flow += f;
  28.             for (; j != s; j = i, i = p[i])
  29.             {
  30.                 R[i][j] -= f;
  31.                 R[j][i] += f;
  32.             }
  33.             // 回到源點,繼續找擴充路徑。
  34.             f = 1e9;
  35.             i = s;
  36.         }
  37.         // 沿著容許邊前進
  38.         else if (j < V)
  39.         {
  40.             pf[j] = f;
  41.             p[j] = i;
  42.             f = min(f, R[i][j]);
  43.             i = j;
  44.         }
  45.         // relabel,並且沿著先前的路徑撤退。
  46.         else
  47.         {
  48.             if (--cnt[h[i]] == 0) h[s] = V;
  49.             ++cnt[++h[i]];
  50.             f = pf[i];
  51.             i = p[i];
  52.         }
  53.     }
  54.     return flow;
  55. }
 
  1. const int V = 100;
  2. int C[V][V], R[V][V];
  3. int h[V], cnt[V+1];
  4.  
  5. int DFS(int i, int df, int s, int t)
  6. {
  7.     // 到達匯點,得到擴充流量大小。
  8.     if (i == t) return df;
  9.  
  10.     // 尋找容許邊
  11.     for (int j=0; j<V; ++j)
  12.         if (R[i][j] > 0 && h[i] == h[j] + 1)
  13.         {
  14.             // 沿著容許邊前進
  15.             int f = DFS(j, min(df, R[i][j]), s, t);
  16.  
  17.             // 到達匯點,擴充流量。
  18.             if (f)
  19.             {
  20.                 R[i][j] -= f;
  21.                 R[j][i] += f;
  22.                 return f;
  23.             }
  24.         }
  25.  
  26.     // relabel,並且沿著先前的路徑撤退。
  27.     if (--cnt[h[i]] == 0) h[s] = V; // 演算法結束
  28.     ++cnt[++h[i]];
  29.  
  30.     return 0;
  31. }
  32.  
  33. int ISAP(int s, int t)
  34. {
  35.     memcpy(R, C, sizeof(R));
  36.     memset(cnt, 0, sizeof(cnt));
  37. //  cnt[0] = V; // 匯點永不升高,高度為零的點永遠存在。
  38.                 // 大可不必關心高度為零的點。
  39.     int flow = 0;
  40.     while (h[s] < V) flow += DFS(s, 1e9, s, t);
  41.     return flow;
  42. }

尋找擴充路徑,沿著容許邊行走,從源點走到匯點,途中各點的高度逐次減一。缺一不可。

任何一種高度出現了、又完全消失之後,表示源點到匯點往後無法貫通,開始 retreat ,可以直接結束演算法。此即 cnt 的功用。

UVa 10983

演算法

引入阻塞流的概念。以 backtracking 的順序而非 DFS 的順序沿著容許邊行走,尋找擴充流而非擴充路徑。

特色是尋找每一點到匯點的阻塞流(水足夠時)、也就是尋找局部的阻塞流!每次回溯皆實施 relabel ,隨時調校局部的最短路徑長度!

時間複雜度 O(V²E) 。

 
  1. const int V = 100;
  2. int C[V][V], R[V][V];
  3. int h[V], cnt[V+1];
  4.  
  5. int DFS(int i, int df, int s, int t)
  6. {
  7.     if (i == t) return df;
  8.  
  9.     int tf = df;
  10.     for (int j=0; j<V; ++j)
  11.         if (R[i][j] > 0 && h[i] == h[j] + 1)
  12.         {
  13.             int f = DFS(j, min(df, R[i][j]), s, t);
  14.  
  15.             // 不斷尋找其他擴充路徑,直到水量不足。
  16.             R[i][j] -= f;
  17.             R[j][i] += f;
  18.             df -= f;
  19.             if (df == 0 || h[S] == V) return tf - df;
  20.         }
  21.  
  22.     // 每次backtrack的時候就relabel!有點類似discharge。
  23.     int newh = h[i];
  24.     for (int j=0; j<V; ++j)
  25.         if (R[i][j] > 0)
  26.             newh = min(newh, h[j]);
  27.  
  28.     if (--cnt[h[i]] == 0) h[s] = V;
  29.     else ++cnt[h[i] = newh + 1];
  30.  
  31.     return tf - df;
  32. }
  33.  
  34. int ISAP(int s, int t)
  35. {
  36.     memcpy(R, C, sizeof(R));
  37.     memset(cnt, 0, sizeof(cnt));
  38. //  cnt[0] = V;
  39.  
  40.     int flow = 0;
  41.     while (h[s] < V) flow += DFS(s, 1e9, s, t);
  42.     return flow;
  43. }

延伸閱讀: height label

有了 height label 的概念之後,我們可以重新審視之前的擴充路徑演算法,給予更簡潔的解釋。

Augmenting Path Algorithm(Ford-Fulkerson Algorithm)
不使用height label。
隨便找擴充路徑。

Shortest Augmenting Path Algorithm(Edmonds-Karp Algorithm)
每回合都用BFS重新建立height label,
同時用BFS找一條擴充路徑。

Blocking Flow Algorithm(Dinic's Algorithm)
每回合都用BFS重新建立height label,
每回合都用多次DFS找擴充路徑(最後形成阻塞流)。

Improved Shortest Augmenting Path Algorithm
一開始用BFS建立height label(也可以全部設定為零),
每回合都用DFS找擴充路徑。

摘要

最大流問題只有四個要素:
 甲、容量(Flow Network)。甲≥0。
 乙、流量(Flow)。甲≥乙≥0。
 丙、剩餘容量(Residual Network)。甲減乙、反向乙。
 丁、遵行方向(Admissible Network)。丁屬於丙:邊兩端高度差為一。

最大流問題:
 給定甲暨源點匯點,令乙趨近甲,求乙。

最大流演算法:
 以丙的視角看問題,有隙就流。(最大流最小割定理)
 以丁的視角看問題,可以縮短流動路線,加速演算法。

最大流演算法有兩大類:
 一、擴充路徑法(率先流到匯點)
 Augmenting Path Algorithm          丙上隨便找一條擴充路徑,不斷找。
 (Ford-Fulkerson Algorithm)         O(EF)

 Shortest Augmenting Path Algo.     丙上BFS找一條擴充路徑,最多VE次。
 (Edmonds-Karp Algorithm)           O(VEE)

 Blocking Flow Algorithm            丁上找擴充流,最多V次。
 (Dinic's Algorithm)                O(VVE)

 Improved Shortest Augmenting       丁上找擴充路徑,最多VE次。
 Path Algorithm                     O(VVE)

 Capacity Scaling Algorithm         限制甲尺度找擴充流,最多logC次。
                                    O(EElogC)

 二、預流推進法(率先流離源點)
 Preflow-Push Algorithm             隨性推  O(VVE)
 Relabel-to-front Algorithm         插隊推  O(VVV)
 FIFO Preflow-Push Algorithm        FIFO推  O(VVV)
 Highest-Label Preflow-Push Algo.   最高推  O(VVV)

Maximum s-t Flow: 
Orlin's Algorithm

http://jorlin.scripts.mit.edu/Max_flows_in_O(nm)_time.html

目前時間複雜度最低的演算法。使用一些糟糕的手法硬湊出來的,缺乏實務價值。

 

猜你喜欢

转载自www.cnblogs.com/nervendnig/p/8927732.html