網絡最大流問題算法小結 [轉]


通過 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 方法描述如下:

Ford-Fulkerson 方法 (G,s,t)
1 將各邊上流量 f 初始化為 0
2 while 存在一條增廣路徑 p
3     do 沿路徑 p 增廣流量 f
4 return f

假設有向網絡 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 的邊構成殘量網絡 G,增廣路徑p即是殘量網絡中從源點 s 到終點 t 的路徑。

沿路徑 p 增廣流量 f 的操作基本都是相同的,各算法的區別就在於尋找增廣路徑 p 的方法不同。例如可以尋找從 s 到 t 的最短路徑,或者流量最大的路徑。

2. Edmonds - Karp 算法

Shortest Augmenting Path (SAP) 是每次尋找最短增廣路的一類算法,Edmonds - Karp 算法以及后來著名的 Dinic 算法都屬於此。SAP 類算法可統一描述如下:

Shortest Augmenting Path
1 x <-- 0
2 while 在殘量網絡 Gx 中存在增廣路 s ~> t
3     do 找一條最短的增廣路徑 P
4        delta <-- min{rij:(i,j) 屬於 P}
5        沿 P 增廣 delta 大小的流量
6        更新殘量網絡 Gx
7 return x

在無權邊的有向圖中尋找最短路,最簡單的方法就是廣度優先搜索 (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 部分算法可描述如下:

 
1 p <-- s
 2 while s 的出度 > 0 do
 3     u <-- p.top
 4     if u != t then
 5         if u 的出度 > 0 then
 6             設 (u,v) 為 AN(f) 中一條邊
 7             p <-- p, v
 8         else
 9             從 p 和 AN(f) 中刪除點 u 以及和 u 連接的所有邊
10     else
11         沿 p 增廣
12         令 p.top 為從 s 沿 p 可到達的最后頂點
13 end while

 

 實際代碼中不必真的用一個圖來存儲分層網絡,只需保存每個頂點的距離標號並在 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}。具體算法描述如下:

algorithm Improved-Shortest-Augmenting-Path
 1 f <-- 0
 2 從終點 t 開始進行一遍反向 BFS 求得所有頂點的起始距離標號 d(i)
 3 i <-- s
 4 while d(s) < n do
 5     if i = t then    // 找到增廣路
 6         Augment
 7         i <-- s      // 從源點 s 開始下次尋找
 8     if Gf 包含從 i 出發的一條允許弧 (i,j)
 9         then Advance(i)
10         else Retreat(i)    // 沒有從 i 出發的允許弧則回退
11 return f

procedure Advance(i)
1 設 (i,j) 為從 i 出發的一條允許弧
2 pi(j) <-- i    // 保存一條反向路徑,為回退時准備
3 i <-- j        // 前進一步,使 j 成為當前結點

procedure Retreat(i)
1 d(i) <-- 1 + min{d(j):(i,j)屬於殘量網絡Gf}    // 稱為重標號的操作
2 if i != s
3     then i <-- pi(i)    // 回退一步

procedure Augment
1 pi 中記錄為當前找到的增廣路 P
2 delta <-- min{rij:(i,j)屬於P}
3 沿路徑 P 增廣 delta 的流量
4 更新殘量網絡 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 的非遞歸版本代碼,網絡采用鄰接表 + 反向弧指針:

 

  1 #include<cstdio>
  2 #include<memory>
  3 using namespace std;
  4  
  5 const int maxnode = 1024;
  6 const int infinity = 2100000000;
  7  
  8 struct edge{
  9     int ver;    // vertex
 10     int cap;    // capacity
 11     int flow;   // current flow in this arc
 12     edge *next; // next arc
 13     edge *rev;  // reverse arc
 14     edge(){}
 15     edge(int Vertex, int Capacity, edge *Next)
 16         :ver(Vertex), cap(Capacity), flow(0), next(Next), rev((edge*)NULL){}
 17     void* operator new(size_t, void *p){
 18         return p;
 19     }
 20 }*Net[maxnode];
 21 int dist[maxnode]= {0}, numbs[maxnode] = {0}, src, des, n;
 22  
 23 void rev_BFS(){
 24     int Q[maxnode], head = 0, tail = 0;
 25     for(int i=1; i<=n; ++i){
 26         dist[i] = maxnode;
 27         numbs[i] = 0;
 28     }
 29  
 30     Q[tail++] = des;
 31     dist[des] = 0;
 32     numbs[0] = 1;
 33     while(head != tail){
 34         int v = Q[head++];
 35         for(edge *e = Net[v]; e; e = e->next){
 36             if(e->rev->cap == 0 || dist[e->ver] < maxnode)continue;
 37             dist[e->ver] = dist[v] + 1;
 38             ++numbs[dist[e->ver]];
 39             Q[tail++] = e->ver;
 40         }
 41     }
 42 }
 43  
 44 int maxflow(){
 45     int u, totalflow = 0;
 46     edge *CurEdge[maxnode], *revpath[maxnode];
 47     for(int i=1; i<=n; ++i)CurEdge[i] = Net[i];
 48     u = src;
 49     while(dist[src] < n){
 50         if(u == des){    // find an augmenting path
 51             int augflow = infinity;
 52             for(int i = src; i != des; i = CurEdge[i]->ver)
 53                 augflow = min(augflow, CurEdge[i]->cap);
 54             for(int i = src; i != des; i = CurEdge[i]->ver){
 55                 CurEdge[i]->cap -= augflow;
 56                 CurEdge[i]->rev->cap += augflow;
 57                 CurEdge[i]->flow += augflow;
 58                 CurEdge[i]->rev->flow -= augflow;
 59             }
 60             totalflow += augflow;
 61             u = src;
 62         }
 63  
 64         edge *e;
 65         for(e = CurEdge[u]; e; e = e->next)
 66             if(e->cap > 0 && dist[u] == dist[e->ver] + 1)break;
 67         if(e){    // find an admissible arc, then Advance
 68             CurEdge[u] = e;
 69             revpath[e->ver] = e->rev;
 70             u = e->ver;
 71         } else {    // no admissible arc, then relabel this vertex
 72             if(0 == (--numbs[dist[u]]))break;    // GAP cut, Important!
 73             CurEdge[u] = Net[u];
 74             int mindist = n;
 75             for(edge *te = Net[u]; te; te = te->next)
 76                 if(te->cap > 0)mindist = min(mindist, dist[te->ver]);
 77             dist[u] = mindist + 1;
 78             ++numbs[dist[u]];
 79             if(u != src)
 80                 u = revpath[u]->ver;    // Backtrack
 81         }
 82     }
 83     return totalflow;
 84 }
 85  
 86 int main(){
 87     int m, u, v, w;
 88     freopen("ditch.in", "r", stdin);
 89     freopen("ditch.out", "w", stdout);
 90     while(scanf("%d%d", &m, &n) != EOF){    // POJ 1273 need this while loop
 91         edge *buffer = new edge[2*m];
 92         edge *data = buffer;
 93         src = 1; des = n;
 94         while(m--){
 95             scanf("%d%d%d", &u, &v, &w);
 96             Net[u] = new((void*) data++) edge(v, w, Net[u]);
 97             Net[v] = new((void*) data++) edge(u, 0, Net[v]);
 98             Net[u]->rev = Net[v];
 99             Net[v]->rev = Net[u];
100         }
101         rev_BFS();
102         printf("%d\n", maxflow());
103         delete [] buffer;
104     }
105     return 0;
106 }

 

[轉] http://dantvt.is-programmer.com/posts/7974.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM