zoukankan      html  css  js  c++  java
  • [转载]网络流笔记

    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

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

     
  • 相关阅读:
    ios des加密
    testng-注解之@BeforeGroups、@AfterGroups +分组测试
    testng -忽略测试
    testng -注解之@BeforeSuite、@AfterSuite
    jmeter创建测试计划,执行post请求
    jmeter进行压力测试
    jmeter在mac电脑上安装使用
    java -获取变量类型
    java中枚举类enum
    java的日期与时间,Date ,SImpleDateFormat(), Calendar
  • 原文地址:https://www.cnblogs.com/nervendnig/p/8927732.html
Copyright © 2011-2022 走看看