圖論專題-學習筆記:dinic 求解最大流
1. 前言
本篇博文講解求解最大流的 dinic 演算法。
在學這篇博文之前,請先確保掌握以下知識:
- 網路流的一些基礎定義,參見:演算法學習筆記:網路流#1——有關內容+演算法導航
- FF 與 EK 求解最大流的思路,參見:演算法學習筆記:網路流#2——EK 求解最大流
下面假設讀者已經掌握上述內容。
先來回顧 EK 求解最大流的思路:利用 BFS 不斷尋找增廣路,不斷推流,直到找不到增廣路為止。
而 FF 求解最大流的思路:利用 DFS 不斷尋找增廣路,不斷推流,直到找不到增廣路為止。
上文中作者提到過,FF 求解最短路效率低下的問題就是因為 DFS 複雜度太高了。
但是:
Dfs 的實現方式雖然暫時無法取得好的效果,但我們並不應該就此放棄,Dfs 相對 Bfs 靈活的架構必能給予我們廣闊的優化空間。——《資訊學奧賽一本通——提高篇》
所以我們要想想如何優化 DFS。
發明 EK 演算法的科學家 Dinic 也意識到了這一點,於是他就在 FF 的基礎上加以改進,提出了大名鼎鼎的 dinic 演算法,也是目前網路流的主流演算法之一,甚至有的人這麼說:99% 的網路流問題,都可以用 dinic 通過。
沒錯,FF 是 Dinic 發明的
接下來就開始講解吧!
2. 模板
模板題:
2.1 詳解
首先我們需要知道 DFS 複雜度為什麼高。
假設當前網路構成了一個環,而且邊的流量都是 \(INF\),於是不管這個環裡面流進了多少流量,終將無休止的迴圈。
那麼怎樣解決這個問題呢?最顯然的一個思路就是控制一下迴圈層數。
當然這樣程式碼太煩了,於是就有另外一個思路:能不能控制一下點的去向呢?
換句話說,能不能控制一下大體流量的走向呢?
這就是 dinic 演算法的主要思路。
dinic 演算法的第一步,便是對網路分層。
換句話說,從源點開始,標記源點深度為 1,其能到達的點深度為 2,以此類推。
然後,再開始 DFS,規定只能從層數小的流向層數大的。
但是這樣仍然有一個問題,看圖:
(繪圖工具:Windows 10 的 Microsoft Whiteboard)
在上圖中,您會發現:如果 1 號點不幸的選擇了 2 號點,那麼就要繞一大圈才能到 \(t\),但是實際上只需要經過 150 號點就可以了呀!
因此為了防止這種情況,規定:流量只能在相鄰兩層之間流動。
這樣就可以完美避免了這個問題,但是正確性呢?
仍然正確!見後面的正確性證明。
現在先回到這個問題,假設有一個點推流推不出去了,這說明什麼?
這說明這個點已經與匯點不在連通了!這個時候,需要將這個點的層數改為 0,節省時間。
2.2 正確性證明
證明如下:
考慮 FF/EK 演算法的終止條件是沒有增廣路,那麼 dinic 也是沒有增廣路。
在這樣分層之後,如果圖中還有增廣路,那麼在 BFS 分層的時候一定會擴充套件到這條增廣路,而最後的終止條件就是沒有增廣路。
證畢。
2.3 程式碼
程式碼如下:
/*
========= Plozia =========
Author:Plozia
Problem:P3376 【模板】網路最大流——dinic 求解
Date:2021/3/18
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::queue;
typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, Head[MAXN], cnt_Edge = 1, dep[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
bool vis[MAXN];
LL ans;
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return (fh == 1) ? sum : -sum;
}
bool bfs()
{
queue <int> q; q.push(s);
memset(vis, 0, sizeof(vis));
memset(dep, 0, sizeof(dep));
dep[s] = 1; vis[s] = 1;
while (!q.empty())
{
int x = q.front(); q.pop();
for (int i = Head[x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (Edge[i].val == 0 || vis[u]) continue;//只考慮有流量的點
vis[u] = 1; dep[u] = dep[x] + 1;
q.push(u);
}
}
return dep[t];
}
LL dfs(int now, LL Flow)
{
if (now == t) return Flow;
LL used = 0;
for (int i = Head[now]; i; i = Edge[i].Next)//能推流就推流
{
int u = Edge[i].to;
if (Edge[i].val && dep[u] == dep[now] + 1)
{
LL Minn = dfs(u, Min(Flow - used, Edge[i].val));
Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
if (used == Flow) return used;
}
}
if (used == 0) dep[now] = 0;//修改層數
return used;
}
int main()
{
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; ++i)
{
int x = read(), y = read(), z = read();
add_Edge(x, y, z); add_Edge(y, x, 0);
}
while (bfs()) ans += dfs(s, INF);//不斷找增廣路
printf("%lld\n", ans);
return 0;
}
2.4 優化
dinic 演算法的很重要的一個優化就是當前弧優化。
當前弧優化的意思是這樣的:
每一次增廣路的時候記錄一下當前走到哪一條邊(弧)了,下一次到這個點推流的時候直接從這條邊開始遍歷。
為什麼是正確的呢?
顯然,已經遍歷過的邊肯定已經推完了所有流量,也就是再推流也沒有用,只有當前這條邊可能可以再推流,那麼從這條邊開始遍歷即可。
當前弧優化的程式碼如下:
/*
========= Plozia =========
Author:Plozia
Problem:P3376 【模板】網路最大流——dinic 求解
Date:2021/3/18
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::queue;
typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
const LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, Head[MAXN], cnt_Edge = 1, dep[MAXN], cur[MAXN];
struct node {int to; LL val; int Next;} Edge[MAXM << 1];
bool vis[MAXN];
LL ans;
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return (fh == 1) ? sum : -sum;
}
bool bfs()
{
queue <int> q; q.push(s);
memset(vis, 0, sizeof(vis));
memset(dep, 0, sizeof(dep));
dep[s] = 1; vis[s] = 1;
while (!q.empty())
{
int x = q.front(); q.pop();
for (int i = Head[x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (Edge[i].val == 0 || vis[u]) continue;//只考慮有流量的點
vis[u] = 1; dep[u] = dep[x] + 1;
q.push(u);
}
}
return dep[t];
}
LL dfs(int now, LL Flow)
{
if (now == t) return Flow;
LL used = 0;
for (int i = cur[now]; i; i = Edge[i].Next)//能推流就推流
{
cur[now] = i;//當前弧優化
int u = Edge[i].to;
if (Edge[i].val && dep[u] == dep[now] + 1)
{
LL Minn = dfs(u, Min(Flow - used, Edge[i].val));
Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
if (used == Flow) return used;
}
}
if (used == 0) dep[now] = 0;//修改層數
return used;
}
int main()
{
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; ++i)
{
int x = read(), y = read(), z = read();
add_Edge(x, y, z); add_Edge(y, x, 0);
}
while (bfs()) {for (int i = 1; i <= n; ++i) cur[i] = Head[i]; ans += dfs(s, INF);}//不斷找增廣路
printf("%lld\n", ans);
return 0;
}
3. 總結
dinic 演算法的基本步驟:利用 BFS 分層,然後利用 DFS 不斷尋找增廣路,能推流就推流。
實際上,dinic 演算法已經足夠高效了,但是很遺憾的是 dinic 在特殊構造的資料下仍然會被卡掉,這個時候就需要一種更高效的演算法:ISAP 出場了!