前言
看到網上Dinic和ISAP的比較,多數人認為ISAP更快,不容易爆棧。當然,也有少數人認為,在多數情況下,Dinic比較穩定。我認為Dinic的思路比ISAP更簡明,所以選擇了Dinic算法
UPD20190626:突然發現這篇博客閱讀量1000多了,把我嚇得不輕,感謝各位網友的支持。
介紹
Dinic算法本身,自然是解決最大流(普通最大流,最大流最小割)的算法。通過處理,也可以解決二分圖的最大匹配(下文介紹),最大權閉合圖。
算法介紹:介紹Dinic之前,我們先介紹一下最大流。在最大流的題目中,圖被稱為"網絡",每條邊的邊權被稱作"流量",有一個起點(源點)和一個終點(匯點)。我們要求的最大流,可以這樣形象地理解:源點有一個水庫,里面有無限噸水(QWQ),匯點也有一個水 庫,希望得到最多的水。我們假設每個河道一天只能輸水n噸(及網絡流中的流量),求解匯點最多能的到幾噸水。再給一個正式的定義:最大流是指網絡中滿足弧流量限制條件和平衡條件且具有最大流量的可行流
下面我們正式介紹Dinic:
首先引出網絡流算法中的鏈,給個正式定義:鏈是網絡中的一個頂點序列,這個序列中前后兩個頂點有弧相連(其實我認為這個定義無關緊要,所以重點看下面弧的定義)。
弧 :弧分為兩種,第一種是前向弧是指方向和鏈一致的弧(簡單的說就是輸入的邊)---前向弧,第二種弧是指方向和鏈不一致的弧(簡單的說就是輸入的邊反一反)---后向弧。
好了接下來要引出一個網絡流算法的重要概念
增廣路
給個正式的定義:
1、增廣路是一條鏈
2、鏈上的前向弧都是非飽和弧
鏈上的后向弧都是非零弧
3、鏈是由源點到匯點的
總結一下:額...這聽起來好像啥都沒說(滑稽)
談談我的理解:
增廣路是一個邊集,是一條從源點到匯點的路徑
增廣路有一個權值表示該增廣路的最大流量,而最大流量的大小是邊集中流量最小的邊的流量。
剩余網絡
由反向弧組成的網絡,關於反向弧的權的問題,后文會介紹。
說了一大堆,下面正式介紹Dinic算法
Dinic算法的大致步驟
1、建立網絡(包括正向弧和反向弧(初始邊權為0)),將總流量置為0
2、構造層次網絡(怎么又有新概念 T_T)
簡單的說,就是求出每個點u的層次,u的層次是從源點到該點的最短路徑(注意:這個最短路是指弧的權都為1的情況下的最短路),若與源點不連通,層次置為-1
一遍BFS輕松解決
3、判斷匯點的層次是否為-1
是:再見,算法結束,輸出當前的總流量
否:下一步
4、用一次DFS完成所有增廣,增廣是什么呢?
增廣(我的理解):通過DFS找上述的增廣路,找到了之后,將每條邊的權都減去該增廣路中擁有最小流量的邊的流量,將每條邊的反向邊的權增加這個值,同時將總流量加上這個值
DFS直到找不到一條可行的從原點到匯點的路
5、goto 步驟2
細節處理,如何快速找到一條邊的反向邊:邊的編號從0開始,反向邊加在正向邊之后,反向邊即為該點的編號異或1
復雜度:理論上來說,最慢應該是O((n^2)*m),n表點數,m表邊數,實際上呢,應該快得不少
代碼實例:(參見洛谷P3376)
傳送門[>洛谷<] 重要提示:您的等級必須達到藍色以上,否則后果自負
當前弧優化
在DFS的時候記錄當前已經計算到第幾條邊了,避免重復計算。
那么具體原理是什么呢?
每當我們發現一條新的增廣路時,由於算法"貪心"的性質,該增廣路上的可用流量其實已經被支配完全(即使放入另一條增廣路也毫無意義)。
那么當我們每次到達一個節點時,必定有許多邊已經被完全支配,又由於我們深度優先搜索的性質,故可以記錄上一次搜索到哪一條邊,下一次直接從該邊開始DFS。
在下一次構建層次網絡時注意將head數組還原
代碼
* 使用當前弧優化
#include <cstdio> #include <cstring> #include <queue> #include <algorithm> using namespace std; const int MAX = (1ll << 31) - 1; int read(){ int x = 0; int zf = 1; char ch = ' '; while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if (ch == '-') zf = -1, ch = getchar(); while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar(); return x * zf; } struct Edge{ int to; int dis; int next; } edges[210000]; int cur[10010], head[10010], edge_num = -1; int n, m, s, t; void addEdge2(int from, int to, int dis){ edges[++edge_num].to = to; edges[edge_num].dis = dis; edges[edge_num].next = head[from]; head[from] = edge_num; } void addEdge(int from, int to, int dis){ addEdge2(from, to, dis), addEdge2(to, from, 0); } int d[10010]; int DFS(int u, int flow){ if (u == t) return flow; int _flow = 0, __flow; for (int& c_e = cur[u]; c_e != -1; c_e = edges[c_e].next){ int v = edges[c_e].to; if (d[v] == d[u] + 1 && edges[c_e].dis > 0){ __flow = DFS(v, min(flow, edges[c_e].dis)); flow -= __flow; edges[c_e].dis -= __flow; _flow += __flow; edges[c_e^1].dis += __flow; if (!flow) break; } } if (!_flow) d[u] = -1; return _flow; } bool BFS(){ memset(d, -1, sizeof(d)); queue<int> que; que.push(s); d[s] = 0; int u, _new; while (!que.empty()){ u = que.front(), que.pop(); for (int c_e = head[u]; c_e != -1; c_e = edges[c_e].next){ _new = edges[c_e].to; if (d[_new] == -1 && edges[c_e].dis > 0){ d[_new] = d[u] + 1; que.push(_new); } } } return (d[t] != -1); } void dinic(){ int max_flow = 0; while (BFS()){ for (int i = 1; i <= n; ++i) cur[i] = head[i]; max_flow += DFS(s, MAX); } printf("%d", max_flow); } int main(){ n = read(), m = read(), s = read(), t = read(); memset(head, -1, sizeof(head)); for (int i = 0; i < m; i++){ int u = read(), v = read(), w = read(); addEdge(u, v, w); } dinic(); return 0; }
算法主要應用場景
1、裸的最大流
2、二分圖的最大匹配:建一個點S,連到二分圖的集合A中;建一個點T,連到二分圖的集合B中。再將所有的集合A中的點與集合B中的點相連。全部邊權設為1,跑一遍最大流,結果即為二分圖的最大匹配
3、最小割(定義自行百度):在單源單匯流量圖中,最大流等於最小割
4、求最大權閉合圖(定義自行百度):最大權值=正點權之和-最小割
主要問題:
為什么要建立反向邊?
Answer:總結多篇博客,認為建立反向邊旨在增加重新調整流的機會,即保障解是最優的(還是沒有理解?可以自行百度:D)。
版權申明:未經博主允許禁止轉載