# Edmonds-Karp 演算法與 Dinic 演算法
接續[最大流-最小割定理](https://hackmd.io/@ShanC/maxflow-mincut-theorem),本篇筆記會講講為什麼需要更好的演算法去找最大流量。總共會聊到兩種演算法 : Edmonds-Karp 演算法與 Dinic 演算法
競程會用的應該只有 Dinic 演算法而已,所以如果只想把這裡當速食店的話讀 Dinic 演算法那段就好
## Ford-Fulkerson 演算法的問題
### 時間複雜度太大 (or 不確定)
在[最大流-最小割定理](https://hackmd.io/@ShanC/maxflow-mincut-theorem)中,有講到 Ford-Fulkerson 演算法,這是由 Ford 與 Fulkerson 在 1954 年提出。然而這演算法最大的問題就在於,其並沒有明確的說明要如何在圖中找出一條 $s$-$t$ 路徑,所以我們其實並不知道實作起來的複雜度為何。~~由於很多數學家不見得有時間複雜度的觀念~~,~~他們很多時候只會說明某某東西存在卻不說要怎麼找~~。演算法的時間複雜度在最差情況下恐怕會來到指數時間 (exponential running time),當然這跟二進制的編碼長度也有關係。因定義不夠明確的緣故,Ford-Fulkerson 演算法有時候會被稱為「方法」而不是「演算法」
### 好的演算法?
那麼 Ford 與 Fulkerson 有提出演算法去解決算法的問題嗎? Fulkerson 算是有,只是用的是線性規劃 (Linear Programming) 的方法,詳情可以看[這篇 paper](https://onlinelibrary.wiley.com/doi/10.1002/nav.3800020407)。想知道線性規劃是什麼可以修修看作業研究 (Operation Research),反正我是沒修過
話雖如此,在之後其他人的研究中仍有人提出好的演算法來解決這個問題,其中就包含許多大師們的傑作。Edmonds 與 Karp 在 1972 年提出的演算法、Dinitz 在 1970 年提出的演算法的變體是至今仍常被使用的演算法
## Edmonds-Karp 演算法
Edmonds-Karp 演算法就只是用 BFS 實作 Ford-Fulkerson 演算法而已,實際上沒有太複雜。由於演算法的部分在[這篇](https://hackmd.io/@ShanC/maxflow-mincut-theorem#Ford-Fulkerson-%E6%BC%94%E7%AE%97%E6%B3%95)講過了,所以直接給程式碼與 Ford-Fulkerson 演算法的 Pseudocode,兩者幾乎一模一樣,卻有著截然不同的複雜度
### 實作程式碼
#### Ford-Fulkerson 演算法的 Pseudocode
```c
FordFulkerson(N, s, t)
let Gf as the residual graph and cf be the residual capacity
for each e in N.E
f(e) = 0
while there exists a path P from s to t in Gf
cf(P) = min{cf(e) | e is in P}
for each e in P
let eR be the reversed edge
if e in N.E
then f(e) = f(e) + cf(P)
else f(eR) = f(eR) - cf(P)
return f
```
#### C++ 實作 Edmonds-Karp 演算法
```cpp
int n;
int cap[MXN][MXN];
vector<int> adj[MXN];
int bfs(int s, int t, vector<int>& parent) {
fill(parent.begin(), parent.end(), -1);
parent[s] = -2;
queue<pair<int, int>> q; // 儲存 {節點, cf(P)}
q.push({s, INF});
while (!q.empty()) {
int cur = q.front().first;
int flow = q.front().second;
q.pop();
for (int next : adj[cur]) {
// 判斷此點是否走過
if (parent[next] == -1 && cap[cur][next]) {
parent[next] = cur;
// new_flow 就是傳路徑上最小的 flow,也就是 cf(P)
int new_flow = min(flow, cap[cur][next]);
if (next == t) // 如果走到 t 就是找到 path 了
return new_flow;
q.push({next, new_flow});
}
}
}
return 0;
}
int Edmonds_Karp(int s, int t) {
int flow = 0;
vector<int> aug_path(n);
int new_flow;
while (new_flow = bfs(s, t, aug_path)) { // 找一條 augmenting path P
flow += new_flow;
int cur = t;
while (cur != s) { // 沿著 augmenting path P 走
int prev = aug_path[cur];
// e = (cur, prev), eR = (prev, cur)
cap[prev][cur] -= new_flow;
cap[cur][prev] += new_flow;
cur = prev;
}
}
return flow;
}
// 程式碼修改自 CP-algorithms
```
### 時間複雜度分析
首先,令 $E$ 代表邊數、$V$ 代表節點數
#### BFS 的時間複雜度
通常流量網路圖中的 $E$ 會比較大,因此 BFS 會是 $O(V+E)=O(E)$
#### 引理 : 最短距離隨著迭代次數單調遞增
證明使用反證法,也就是假設「最短距離隨著迭代次數單調遞減」。考慮 $f$ 是在迭代前的 flow,會符合假設,而 $f'$ 是迭代後的 flow。根據假設,會存在一個點 $v$ 使得迭代前後 $s, v$ 的最短距離呈現不等式 $dis_{f'}(s, v) < dis_{f}(s, v)$ ...(1)。令在迭代後 $s, v$ 最短路徑再找一個點 $u$ 使得 $dis_{f'}(s, u) = dis_{f'}(s, v) - 1$ ...(2)
<img src="https://hackmd.io/_uploads/B1Z3B6bXeg.png" style="margin: 0 auto; display: block; width: 400px">
$~$
因為在 $f$ 時會挑選 $s, v$ 路徑 (紅色) 走,而不是選擇藉由 $s, u$ 路徑走到 $v$,因此迭代後到 $u$ 的最短距離不會減少 $dis_{f'}(s, u) \geq dis_{f}(s, u)$ ...(3)
根據最短距離的函數會符合[三角不等式](https://hackmd.io/@ShanC/Dijkstra#%E9%AC%86%E5%BC%9B-Relax),可以得到 : $dis_f(s, v) \leq dis_f(s, u) + 1$
$\Rightarrow dis_f(s, u) \geq dis_f(s, v) - 1$ ...(4)
由 (3)、(4) : $dis_{f'}(s, u) \geq dis_f(s, v) - 1$
$\Rightarrow dis_{f}(s, v) \leq dis_{f'}(s, u) + 1$ ...(5)
由 (2)、(5) : $dis_{f}(s, v) \leq dis_{f'}(s, v)$ ...(6)
我們以上的推論都是正確的,然而,這條不等式 (6) 違背原本的假設 (1),因此假設是錯的,所以我們得到「最短距離隨著迭代次數單調遞增」
#### while 迭代次數
首先,定義一條邊是 critical 若且為若他的容量等於剩餘路徑的容量。每次找到一條 augmenting path 時,就會出現 critical 的邊。令 $u, v$ 之間有邊連接,則 $dis_{f}(s, v) = dis_{f}(s, u) + 1$
一旦在迭代中 $f$ 把 $(u, v)$ 填滿了容量,這條邊就會從 residual graph 消失。這條邊如果要再出現的話,僅有可能是 $(v, u)$ 出現在 augmenting path 上。令 $f'$ 就是屬於這種狀況,則會出現等式 $dis_{f'}(s, u) = dis_{f'}(s, v) + 1$
由引理 $dis_f(s, v) \leq dis_{f'}(s, v)$ 代入等式可得 : $dis_{f'}(s, u)\geq dis_{f}(s, v) + 1$
再結合上一個等式 $dis_{f}(s, v) = dis_{f}(s, u) + 1$ 可得 : $dis_{f'}(s, u)\geq dis_{f}(s, u) + 2$
這不等式說明 $(u, v)$ 從第一次成為 critical 到下一次成為 critical,$s$ 到 $u$ 的距離最少會增加 $2$
從 $s$ 到 $u$ 的路徑不包含 $s, u$,其中的節點數會是 $V-2$,因此推得一條邊成為 critical 的次數最多是 $\cfrac{V-2}{2}=O(V)$
一張圖總共有 $E$ 條邊,因此總迭代次數為 $E\times O(V)=O(VE)$
#### 總時間複雜度
- BFS 的時間複雜度 : $O(E)$
- 總共迭代次數 : $O(VE)$
總時間複雜度 : $O(VE)\times O(E)=O(VE^2)$
## Dinic 演算法
首先要說明的是,發明這個演算法的人叫做 Dinitz,只是當時 Even 在推廣演算法時,一直被錯誤的讀音念成 Dinic,漸漸地,這也成為這演算法的名稱
Dinic 演算法可以說是 Edmonds-Karp 演算法的延伸,但其實兩種演算法的發明者都沒有參考彼此的想法,都是各自獨立研究出來
### Dinic 的主要想法
假設樂奈喵想要吃抹茶パフェ,她只知道抹茶パフェ位於東方,那該如何走才能吃到呢?
樂奈喵只有可能走三種方位 : 東、東北、東南,而其他方位並不會被考慮
<img src="https://hackmd.io/_uploads/SJeZNyA-Xlx.png" style="margin: 0 auto; display: block; width: 600px">
$~$
Dinic 演算法的想法就有點像這樣,用類似 heuristic 的方法去避免不可能走的路徑。事實上,演算法所利用的就是 BFS 會產生的性質 - **分層**
### 分層圖 Level Graph
以下圖 $N$ 為例,$s$ 是源點、$t$ 是匯點,實邊是原本的邊、虛線邊是 residual 邊
<img src="https://hackmd.io/_uploads/HybJB0W7xx.png" style="margin: 0 auto; display: block; width: 650px">
$~$
假設從 $s$ 出發開始 BFS,那就可以得到分層圖
同時透過走訪整張圖,也可以確認從 $s$ 到 $t$ 是否存在一條路徑
<img src="https://hackmd.io/_uploads/HkQ_L0b7ge.png" style="margin: 0 auto; display: block; width: 650px">
這時我們可以由分層圖知道,若要走從 $s$ 到 $t$ 的路徑,那麼每走到下一個節點,level 都要嚴格遞增。因此我們把沒有遞減的邊拿掉 (實作上忽略就好),並用 DFS 走訪,就可以找到 augmenting path
<img src="https://hackmd.io/_uploads/H10DdRb7ee.png" style="margin: 0 auto; display: block; width: 650px">
之後的步驟就是做邊的加值或減值,然後重複以上步驟
### 演算法步驟
1. 用 BFS 建立 level graph,並搜尋是否有路徑到 $t$。若沒有就離開迴圈,回傳 flow
2. 用合法的邊進行 DFS,並一邊計算每條邊的 flow 值
3. 重複 1~2
其實跟 Edmonds-Karp 的想法差不多,只是多了 level graph 作為限制
### 實作程式碼
```cpp
const int MXN = 2e3 + 5;
const int INF = INT_MAX;
// 每條邊都記錄三個訊息 : 到哪裡、流量、反邊存在哪
struct Edge {
int v, flow, re_id;
};
int n, s, t, m = 0, level[MXN];
vector<Edge> E[MXN]; // 把兩種圖都建在同一個上面
void add_edge(int u, int v, int f) {
E[u].push_back({v, f, (int)E[v].size()}); // 原本的 graph
E[v].push_back({u, 0, (int)E[u].size() - 1}); // residual graph
}
bool BFS() {
// level 就是 BFS 樹高
for (int i = 0; i < n; i++)
level[i] = -1;
queue<int> que;
que.push(s);
level[s] = 0;
while (!que.empty()) {
int u = que.front();
que.pop();
for (Edge &it : E[u]) {
// 每次就看可以流的邊
// 讓他們的距離 + 1
if (it.flow > 0 && level[it.v] == -1) {
level[it.v] = level[u] + 1;
que.push(it.v);
}
}
}
// 如果 t 能夠被走到,代表一定有 augmenting path
return level[t] != -1;
}
int DFS(int u, int nf) {
if (u == t)
return nf;
int res = 0;
for (auto &it : E[u]) {
// 這條邊必須是原本圖上的,並且要符合 level 性質
if (it.flow > 0 && level[it.v] == level[u] + 1) {
// 這些加值與減值要自己對照著看囉
int tf = DFS(it.v, min(nf, it.flow));
res += tf;
nf -= tf;
it.flow -= tf;
E[it.v][it.re_id].flow += tf;
if (nf == 0)
return res;
}
}
// 若 res 為 0 則代表沒有邊可以走到 u
if (!res)
level[u] = -1;
return res;
}
int dinic(int res = 0) {
// BFS 找 level 並判斷能不能走到 t
while (BFS())
// DFS 從 level 圖中找到一條 augmenting path 並計算 flow
res += DFS(s, INF);
return res;
}
// 修改自海大競程講義
// 此程式碼不含當前弧優化
```
### 當前弧優化
在 DFS 時,會發現有些死路被檢查過很多次,這會降低演算法的效率。因此可以利用減枝 (pruning) 的方法。簡單來說,就是再搞一個指標陣列去維護某些東西,非常簡單的一個做法。然而這份模板畢竟是 for 競賽用途,天機不可洩漏,在這就不放優化版的 code,詳細可以自己查
P.S. ShanC 很好心地在這邊這邊放優化的[那篇論文](https://epubs.siam.org/doi/10.1137/0204043)唯一有提到減枝的地方
<img src="https://hackmd.io/_uploads/B1TLYwfmlx.png" style="margin: 0 auto; display: block">
### 時間複雜度
這裡的證明有點像是在講廢話,所以只講結論
#### 階段數量
- 引理 1 :
從 $s$ 到每個頂點的距離在每次迭代後都不會減少,即 $level_{i+1}[v] \ge level_i[v]$
- 引理 2 :
從 $s$ 到匯點 $t$ 的距離是嚴格增加的,即 $level_{i+1}[t] > level_i[t]$
這個演算法會分成好幾個階段,從這兩個引理可以得出結論,階段數量少於 $V$。因為 $level[t]$ 每次階段都會增加,但它不可能大於 $V-1$
#### 每一階段 DFS 總時間
- 尋找阻塞流的方法是使用 DFS 從 $s$ 推送流到 $t$,直到無法再推送為止
- 為了提高效率,需要移除那些無法再用於推送的邊。可以通過在每個頂點維護一個指標陣列,指向下一條可以使用的邊來實現
- 單次 DFS 執行需要 $O(k+V)$ 時間,其中 $k$ 是指標推進的次數
- 所有 DFS 執行的指標推進總數不超過 $E$。另一方面,執行總次數不超過 $E$,因為每次執行都會少一條邊 (容量滿)
每一階段 DFS 總時間 : $O(VE)$
#### 總時間複雜度
$O(V \cdot VE) = O(V^2E)$
### 特例 : $0$/$1$ 流量網路
Even 跟 Tarjan 在 [1975 年的 paper](https://epubs.siam.org/doi/10.1137/0204043) 裡有證明在容量只有 $0$ 或是 $1$ 的流量網路圖裡面,時間複雜度會有所不同。由於證明太長,就不放上來了,簡短版可以看[這篇](https://cp-algorithms.com/graph/dinic.html)
#### 情形 1 : 容量為單位容量 ($0$ 或 $1$)
這種情形有兩種不同的評估方式,分別會跑出兩種不同的複雜度
- 最多只會有 $\sqrt E$ 次迭代 : $O(E\sqrt E)$
- 最多只會有 $V^{\frac{2}{3}}$ 個階段 : $O(EV^{\frac{2}{3}})$
這種評估可以用在 [Menger 定理](https://hackmd.io/@ShanC/Menger-Theorem#Menger-%E5%AE%9A%E7%90%86)描述的圖當中
#### 情形 2 : 單位容量,且入邊或出邊唯一
用流量網路解[二分圖匹配](https://hackmd.io/@ShanC/bipartite-and-flow#%E6%9C%80%E5%A4%A7%E6%B5%81-%E6%9C%80%E5%B0%8F%E5%89%B2%E5%AE%9A%E7%90%86%E8%88%87-K%C5%91nig-Egerv%C3%A1ry-%E5%AE%9A%E7%90%86)就是屬於這種情況
- 時間複雜度 : $O(E\sqrt V)$
## 一些歷史
這段主要參考自 [Dinitz 親自闢謠](https://link.springer.com/chapter/10.1007/11685654_10)
如果去查論文發表時間的話,可能會發現一點。邏輯上,Dinic 演算法算是利用 Edmonds-Karp 演算法的機制,所以應該是 Edmonds-Karp 演算法要比較早發表。然而,事實正好相反,Dinic 演算法是 1969 年被發明,1970 年發表在 [Doklady Akademii Nauk SSSR](https://en.wikipedia.org/wiki/Proceedings_of_the_USSR_Academy_of_Sciences),而 Edmonds-Karp 演算法則是在 1972 年發表。兩者是各自獨立發表,沒有任何關聯
在 1970 年代,美蘇冷戰的緣故,導致資訊傳遞不發達。西方世界不容易知道蘇聯內部的技術發展到什麼階段。西方的 Shimon Even 與他的學生 Alon Itai,想要讀懂 Dinitz 發表的演算法,然而礙於期刊限制只能寫四頁,要讀懂實在不容易,因此他們花了三天的時間研究,最後理解了 Dinitz 的作法。
Shimon Even 之後就一直在課堂上推廣這套算法,並且誤將 "Dinitz" 念成 "Dinic"。從此,西方世界都稱這個演算法為 Dinic's algorithm。然而,"Dinic's algorithm" 與原本的 "Dinitz's algorithm" 其實有些不同。現在我們競程常用的其實是 Even 跟 Itai 弄出來的版本,而不是原版,就...還挺搞笑的 XD
----
## 參考資料
- D.B.West - Introduction to Graph Theory 2/e
- Introduction to Algorithms, Fourth Edition
- [Ford and Fulkerson - Maximal flow through a network. Canadian Journal of Mathematics](https://archive.org/details/sim_canadian-journal-of-mathematics_1956_8_3/page/398/mode/2up)
- [Schrijver - Paths and Flows - a Historical Survey](https://ir.cwi.nl/pub/18229/18229B.pdf)
- [Edmonds and Karp - Theoretical Improvements in Algorithmic Efficiency for Network Flow Problems](https://web.eecs.umich.edu/~pettie/matching/Edmonds-Karp-network-flow.pdf)
- [Even and Tarjan - Network Flow and Testing Graph Connectivity](https://epubs.siam.org/doi/10.1137/0204043)
- [台師大演算法筆記 - flow](https://web.ntnu.edu.tw/~algo/Flow.html)
- [維基百科 - Edmonds–Karp algorithm](https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm)
- [維基百科 - Dinic's algorithm](https://en.wikipedia.org/wiki/Dinic%27s_algorithm)
- [CP Algorithms - Maximum flow - Ford-Fulkerson and Edmonds-Karp](https://cp-algorithms.com/graph/edmonds_karp.html)
- [CP Algorithms - Maximum flow - Dinic's algorithm](https://cp-algorithms.com/graph/dinic.html)
- [Brilliant - Edmonds-Karp Algorithm](https://brilliant.org/wiki/edmonds-karp-algorithm/)
- [海大競程 - 匹配與網路流](https://hackmd.io/@LeeShoWhaodian/flow#/) (裡面似乎有點錯)
- [吃手手嚇到 - Edmonds-Karp 演算法](https://hackmd.io/@IiFm7xjrSOC3EYPOk9Mx4A/Hyekh6h39)
- [Dinic's Algorithm | Network Flow | Graph Theory](https://youtu.be/M6cm8UeeziI?si=qEYSdYffwn_bDswO)
- [Dinitz’ Algorithm: The Original Version and Even’s Version](https://link.springer.com/chapter/10.1007/11685654_10)
----
> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2025/6/9