【轉】USACO 4.2.1 Ditch 網路最大流問題演算法小結
原地址:http://dantvt.is-programmer.com/posts/7974.html
出處:http://dantvt.is-programmer.com/(輝夜の連珠 - Lunatic Princess)/作者:輝夜(tadvent)
一般我轉載文章前都會附帶一段吐槽,這次也不例外。
於是,本次的吐槽是……
1.作者竟然是輝夜同好……副標題居然也是【Lunatic Princes】果然說道輝夜就會想到竹取飛翔麼XD
(↑看不懂此人在說啥的請無視)
2.天哪,網路流還有一個叫Improved SAP的演算法,我對網路流的瞭解到Dinic就止步了啊【撓牆】
3.……真相見回複的2樓
以下為本文
------------------------------------------------------------
通過 USACO 4.2.1 Ditch 學習一下最大流演算法 。可惜它給的測試資料幾乎沒有任何殺傷力,後面測試時我們採用 DD_engi 寫的程式產生的加強版資料。
總體上來說,最大流演算法分為兩大類:增廣路 (Augmenting Path) 和預流推進重標號 (Push Relabel) 。也有演算法同時借鑒了兩者的長處,如 Improved SAP 。本篇主要介紹增廣路類演算法,思想、複雜度及實際運行效率比較,並試圖從中選擇一種兼顧代碼複雜度和運行效率的較好方案。以下我們將會看到,有時理論分析的時間複雜度並不能很好的反映一種演算法的實際效率。
1. Ford - Fulkerson 方法
所有增廣路演算法的基礎都是 Ford - Fulkerson 方法。稱之為方法而不是演算法是因為 Ford - Fulkerson 只提供了一類思想,在此之上的具體操作可有不同的實現方案。
給定一個有向網路 G(V,E) 以及源點 s 終點 t ,FF 方法描述如下:
假設有向網路 G 中邊 (i,j) 的容量為 c(i,j) ,當前流量為 f(i,j) ,則此邊的剩餘流量即為 r(i,j) = c(i,j) - f(i,j) ,其反向邊的剩餘流量為 r(j,i) = f(i,j) 。有向網中所有剩餘流量 r(i,j) > 0 的邊構成殘量網路 Gf ,增廣路徑p即是殘量網路中從源點 s 到終點 t 的路徑。
沿路徑 p 增廣流量 f 的操作基本都是相同的,各演算法的區別就在於尋找增廣路徑 p 的方法不同。例如可以尋找從 s 到 t 的最短路徑,或者流量最大的路徑。
2. Edmonds - Karp 演算法
Shortest Augmenting Path (SAP) 是每次尋找最短增廣路的一類演算法,Edmonds - Karp 演算法以及後來著名的 Dinic 演算法都屬於此。SAP 類演算法可統一描述如下:
在無權邊的有向圖中尋找最短路,最簡單的方法就是廣度優先搜尋 (BFS),E-K 演算法就直接來源於此。每次用一遍 BFS 尋找從源點 s 到終點 t 的最短路作為增廣路徑,然後增廣流量 f 並修改殘量網路,直到不存在新的增廣路徑。
E-K 演算法的時間複雜度為 O(VE2),由於 BFS 要搜尋全部小於最短距離的分支路徑之後才能找到終點,因此可以想象頻繁的 BFS 效率是比較低的。實踐中此演算法使用的機會較少。
3. Dinic 演算法
BFS 尋找終點太慢,而 DFS 又不能保證找到最短路徑。1970年 Dinic 提出一種思想,結合了 BFS 與 DFS 的優勢,採用構造分層網路的方法可以較快找到最短增廣路,此演算法又稱為阻塞流演算法 (Blocking Flow Algorithm)。
首先定義分層網路 AN(f)。在殘量網路中從源點 s 起始進行 BFS,這樣每個頂點在 BFS 樹中會得到一個距源點 s 的距離 d,如 d(s) = 0,直接從 s 出發可到達的點距離為 1,下一層距離為2 ... 。稱所有具有相同距離的頂點位於同一層,在分層網路中,只保留滿足條件 d(i) + 1 = d(j) 的邊,這樣在分層網路中的任意路徑就成為到達此頂點的最短路徑。
Dinic 演算法每次用一遍 BFS 構建分層網路 AN(f),然後在 AN(f) 中一遍 DFS 找到所有到終點 t 的路徑增廣;之後重新構造 AN(f),若終點 t 不在 AN(f) 中則演算法結束。DFS 部分演算法可描述如下:
實際代碼中不必真的用一個圖來儲存分層網路,只需儲存每個頂點的距離標號並在 DFS 時判斷 dist[i] + 1 = dist[j] 即可。Dinic 的時間複雜度為 O(V2E)。由於較少的代碼量和不錯的運行效率,Dinic 在實踐中比較常用。具體代碼可參考 DD_engi 網路流演算法評測包中的標程,這幾天 dinic 演算法的實現一共看過和比較過將近 10 個版本了,DD 寫的那個在效率上是數一數二的,邏輯上也比較清晰。
4. Improved SAP 演算法
本次介紹的重頭戲。通常的 SAP 類演算法在尋找增廣路時總要先進行 BFS,BFS 的最壞情況下複雜度為 O(E),這樣使得普通 SAP 類演算法最壞情況下時間複雜度達到了 O(VE2)。為了避免這種情況,Ahuja 和 Orlin 在1987年提出了Improved SAP 演算法,它充分利用了距離標號的作用,每次發現頂點無出弧時不是像 Dinic 演算法那樣到最後進行 BFS,而是就地對頂點距離重標號,這樣相當於在遍曆的同時順便構建了新的分層網路,每輪尋找之間不必再插入全圖的 BFS 操作,極大提高了運行效率。國內一般把這個演算法稱為 SAP...顯然這是不準確的,畢竟從字面意思上來看 E-K 和 Dinic 都屬於 SAP,我還是習慣稱為 ISAP 或改進的 SAP 演算法。
與 Dinic 演算法不同,ISAP 中的距離標號是每個頂點到達終點 t 的距離。同樣也不需顯式構造分層網路,只要儲存每個頂點的距離標號即可。程式開始時用一個反向 BFS 初始化所有頂點的距離標號,之後從源點開始,進行如下三種操作:(1)當前頂點 i 為終點時增廣 (2) 當前頂點有滿足 dist[i] = dist[j] + 1 的出弧時前進 (3) 當前頂點無滿足條件的出弧時重標號並回退一步。整個迴圈當源點 s 的距離標號 dist[s] >= n 時結束。對 i 點的重標號操作可概括為 dist[i] = 1 + min{dist[j] : (i,j)屬於殘量網路Gf}。具體演算法描述如下:
演算法中的允許弧是指在殘量網路中滿足 dist[i] = dist[j] + 1 的弧。Retreat 過程中若從 i 出發沒有弧屬於殘量網路 Gf 則把頂點距離重標號為 n 。
雖然 ISAP 演算法時間複雜度與 Dinic 相同都是 O(V2E),但在實際表現中要好得多。要提的一點是關於 ISAP 的一個所謂 GAP 最佳化。由於從 s 到 t 的一條最短路徑的頂點距離標號單調遞減,且相鄰頂點標號差嚴格等於1,因此可以預見如果在當前網路中距離標號為 k (0 <= k < n) 的頂點數為 0,那麼可以知道一定不存在一條從 s 到 t 的增廣路徑,此時可直接跳出主迴圈。在我的實測中,這個最佳化是絕對不能少的,一方面可以提高速度,另外可增強 ISAP 演算法時間上的穩定性,不然某些情況下 ISAP 會出奇的費時,而且大大慢於 Dinic 演算法。相對的,初始的一遍 BFS 卻是可有可無,因為 ISAP 可在迴圈中自動建立起分層網路。實測加不加 BFS 已耗用時間差只有 5% 左右,代碼量可節省 15~20 行。
5. 最大容量路徑演算法 (Maximum Capacity Path Algorithm)
1972年還是那個 E-K 組合提出的另一種最大流演算法。每次尋找增廣路徑時不找最短路徑,而找容量最大的。可以預見,此方法與 SAP 類演算法相比可更快逼近最大流,從而降低增廣操作的次數。實際演算法也很簡單,只用把前面 E-K 演算法的 BFS 部分替換為一個類 Dijkstra 演算法即可。USACO 4.2 節的說明詳細介紹了此演算法,這裡就不詳述了。
時間複雜度方面。BFS 是 O(E),簡單 Dijkstra 是 O(V2),因此效果可想而知。但提到 Dijkstra 就不能不提那個 Heap 最佳化,雖然 USACO 的演算法例子中沒有用 Heap ,我自己還是實現了一個加 Heap 的版本,畢竟 STL 的優先隊列太好用了不加白不加啊。效果也是非常明顯的,但比起 Dinic 或 ISAP 仍然存在海量差距,這裡就不再詳細介紹了。
6. Capacity Scaling Algorithm
不知道怎麼翻比較好,索性就這麼放著吧。叫什麼的都有,容量縮放演算法、容量變尺度演算法等,反正就那個意思。類似於二分尋找的思想,尋找增廣路時不必非要局限於尋找最大容量,而是找到一個可接受的較大值即可,一方面有效降低尋找增廣路時的複雜度,另一方面增廣操作次數也不會增加太多。時間複雜度 O(E2logU) 實際效率嘛大約稍好於最前面 BFS 的 E-K 演算法,稀疏圖時表現較優,但仍然不敵 Dinic 與 ISAP。
7. 演算法效率實測!
重頭戲之二,雖然引用比較多,哎~
首先引用此篇強文 《Maximum Flow: Augmenting Path Algorithms Comparison》
對以上演算法在稀疏圖、中等稠密圖及稠密圖上分別進行了對比測試。直接看結果吧:
稀疏圖:
ISAP 輕鬆拿下第一的位置,圖中最左邊的 SAP 應該指的是 E-K 演算法,這裡沒有比較 Dinic 演算法是個小遺憾吧,他把 Dinic 與 SAP 歸為一類了。最大流量路徑的簡單 Dijkstra 實現實在是太失敗了 - -,好在 Heap 最佳化後還比較能接受……可以看到 Scaling 演算法也有不錯的表現。
中等稠密圖:
ISAP 依然領先。最大流量演算法依然不太好過……幾個 Scaling 類演算法仍然可接受。
稠密圖:
ISAP……你無敵了!這次可以看出 BFS 的 Scaling 比 DFS 實現好得多,而且幾乎與 Improved Scaling 不相上下,代碼量卻差不少。看來除 ISAP 外 BFS Scaling 也是個不錯的選擇。
最後補個我自己實測的圖,比較演算法有很多是 DD 網路流演算法評測包裡的標程,評測系統用的 Cena,評測資料為 DD ditch 資料產生程式產生的加強版資料:
我一共寫了 7 個版本的 ISAP ,Dinic 演算法也寫了幾個遞迴版的但效率都不高,只放上來一個算了。從來看似乎 ISAP 全面超越了號稱最大流最快速演算法的 HLPP,但在另外一台機器上測試結果與此卻不大相同,有時 ISAP 與 HLPP 基本持平,有時又稍稍慢一些。在這種差距非常小的情況下似乎編譯器的效果也比較明顯。那個 HLPP 是用 PASCAL 寫的,我不知在 Win32 平台下編譯代碼效率如何,至少我的幾個 ISAP 用 VC2008 + SP1 編譯比用 g++ 要強不少,也可能是參數設定的問題。
不過這些都是小事,關鍵是見證了 ISAP 的實際效率。從上面可以看出不加 GAP 最佳化的 ISAP 有幾個測試點乾脆無法通過,而不加 BFS 卻無甚大影響,遞迴與非遞迴相差在 7% 左右的樣子。綜合以上表現,推薦採用 ISAP 不加 BFS,非遞迴 + GAP 最佳化的寫法,Ditch 這道題一共也才 80 行左右代碼。要提的一點是 GAP 最佳化用遞迴來表現的話不如 while 迴圈來得直接。另外,看起來 HLPP 表現確實很優秀,有機會也好好研究一下吧,預流推進重標號演算法也是一大類呢……
最後附上一個 ISAP + GAP + BFS 的非遞迴版本代碼,網路採用鄰接表 + 反向弧指標: