網路流簡介
基本定義
網路(Network)在圖論中指一個有向圖 \(G=(V,E)\),圖上的每一條邊都有一個值,叫做容量(Capacity),也有兩個特殊點:源點(Source)和匯點(Sink)。
而對於一張網路 \(G\),流(Flow)指的是一個函式 \(f\),\(f(u, v)\) 表示邊 \(u \to v\) 經過的流量,一個點 \(u\) 的淨流量可以表示為 \(F(u) = \textstyle{\sum_{x \in V}} f(u, x) - \textstyle{\sum_{x \in V}} f(x, u)\) 。
每一張網路的流從源點 \(st\) 出發,流向匯點 \(ed\),對於流經的每一條邊,都滿足以下性質:
-
對於一條邊 \(u \to v\) 的容量 \(c(u, v)\),始終有:\(0 \le f(u, v) \le c(u, v)\)。
-
對於除源匯點以外的任意一點,淨流量均為 \(0\)。
由上述性質可知,源匯點的淨流量均不為 \(0\),並且 \(\left | F(st) \right | = \left | F(ed) \right |\),我們不妨定義整張網路的流量為 \(st\) 的淨流量 \(F(st)\),這便是網路流。
常見型別
-
最大流:對於網路 \(G = (V, E)\),給每條邊一定的流量,使得網路流盡可能大,此時 \(F\) 為 \(G\) 的最大流。
-
最小割:對於網路 \(G = (V, E)\),找到 \(st \cdot ed\) 的割 \(\left \{ S, T \right \}\),使得其儘可能小,此時割 \(\left \{ S, T \right \}\) 為 \(G\) 的最小割。
對於網路 \(G = (V,E)\),如果 \(\left \{ S, T \right \}\) 是點集 \(V\) 的劃分(通俗說就是分為了兩部分),即 \(S \cup T = V\) 並且 \(S \cap T = \varnothing\)。如果滿足 \(st \in S,ed \in T\),我們稱 \(\left \{ S, T \right \}\) 是 \(G\) 的一個割(Cut)。一個割具有容量,可以表示為:\(\left \| S,T \right \| \gets \textstyle{\sum_{u \in S}} \textstyle{ \sum_{v \in T}} \ c(u, v)\)。
最大流
講解演算法之前,先給出增廣路(Augmenting Path)的定義:
對於邊 \(u \to v\),如果它的 \(f(u, v) \lt c(u, v)\),就說明這條邊有剩餘容量,為 \(c_f(u, v)\)。一條增廣路指從源點 \(st\) 到匯點 \(ed\) 的一條路徑,所有經過的邊都有剩餘容量。
不難發現,僅當不存在增廣路時,網路流才可能是最大的。
Ford-Fulkerson 演算法
Ford-Fulkerson 演算法(簡稱 FF 演算法)並不是直接用來求解最大流的,而是尋找並更新增廣路,從而求得。
如果單純的貪心,會發現不同的更新順序會導致不同的結果,然而確定次序十分複雜,FF 演算法便是採取了反悔的方式:
-
建圖時對於每條邊 \(u \to v\) 同時建立其反向邊 \(u \gets v\),只不過 \(c(v, u) = c_f(v, u) \gets 0\)。
-
每一次更新增廣路時,如果邊 \(u \to v\) 流過 \(\Delta f\) 的流量,使:
- 如上反覆找到圖中增廣路並更新,直到不存在增廣路為止。
像這樣,透過推流操作帶來的抵消效果使得我們在貪心的時候可以反悔,無需正確的選擇也可以得到最優解。這就是 Ford–Fulkerson 演算法的增廣過程。
證明詳見 最大流 - OI Wiki (oi-wiki.org)。
Edmonds–Karp 演算法
Edmonds–Karp 演算法(簡稱 EK 演算法)便是 FF 增廣的典型應用。
演算法過程
EK 演算法運用 BFS 的方式,每一次遍歷 \(G\) 時,找到一條新的增廣路,使用 FF 增廣更新流 \(f\) 以及相應的邊,得到新的殘餘容量子圖 \(G'\),不斷更新,直至增廣路不存在。
時間複雜度分析
這似乎是一種最原始的暴力,時間複雜度似乎由於更新的次數難以保證。由於其證明難度極其複雜,這裡不便贅述,詳見 最大流 - OI Wiki (oi-wiki.org)。增廣總次數為 \(O(|V| |E|)\),單次 BFS 增廣的時間上界為 \(O(| E |)\),因此總的時間複雜度為 \(O(|V| |E|^2)\)。
程式碼實現
bool bfs()
{
memset(vis, 0, sizeof vis);
hh = 0, tt = -1;
dist[st] = INF, vis[st] = true;
q[++ tt] = st;
while (hh <= tt)
{
int ver = q[hh ++];
for (int i = h[ver]; ~i; i = ne[i])
{
int to = e[i];
if (vis[to] || !w[i]) continue;
dist[to] = min(dist[ver], w[i]);
pre[to] = i;
q[++ tt] = to, vis[to] = true;
if (to == ed) return true;
}
}
return false;
}
void update()
{
int cur = ed;
while (cur != st)
{
int nxt = pre[cur];
w[nxt] -= dist[ed], w[nxt ^ 1] += dist[ed];
cur = e[nxt ^ 1];
}
ans += dist[ed];
}
signed main()
{
// ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m >> st >> ed;
while (m --)
{
int a, b, c; cin >> a >> b >> c;
if (edge[a][b]) w[edge[a][b]] += c;
else add(a, b, c), add(b, a, 0), edge[a][b] = idx - 1;
}
while (bfs()) update();
cout << ans << '\n';
return 0;
}
Dinic 演算法
Dinic 演算法與 EK 演算法不同,但本質上都運用到了 FF 增廣的思想。
演算法過程
由於網路流只能單向傳輸,每條邊只能從一邊流經,我們以源點為起點建立與源點距離有關的分層圖,從而忽略掉許多不可能增廣的邊。分層圖可以透過 BFS 求得。
bool bfs()
{
memset(vis, 0, sizeof vis);
hh = 0, tt = -1;
dist[st] = INF, vis[st] = true;
q[++ tt] = st;
while (hh <= tt)
{
int ver = q[hh ++];
for (int i = h[ver]; ~i; i = ne[i])
{
int to = e[i];
if (vis[to] || !w[i]) continue;
dist[to] = min(dist[ver], w[i]);
pre[to] = i;
q[++ tt] = to, vis[to] = true;
if (to == ed) return true;
}
}
return false;
}
在求出的分層圖上,不斷的 DFS 求出剩餘的增廣路 \(f'\) 並且並列到原來的流 \(f\) 中,直到 BFS 時不存在從源點到匯點的增廣路,此時的流 \(f\) 便是最大流。
時間複雜度分析
透過一系列複雜的證明可知,單次 DFS 的時間複雜度為 \(O(|V| |E|)\),分層圖最多層數為 \(O(|V|)\),因此 Dinic 演算法的時間複雜度上界為 \(O(|V|^2 |E|)\)。
值得注意的是,如果整張圖的容量均為 \(1\),Dinic 演算法的時間複雜度可以達到更優。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define DEBUG
#define x first
#define y second
#define File(a) freopen(a".in", "r", stdin); freopen(a".out", "w", stdout)
typedef long long LL;
typedef pair<int, int> PII;
const int N = 400, M = 10010;
const int INF = 0x3f3f3f3f;
int n, m, st, ed;
int cpy[N], h[N], e[M], ne[M], w[M], idx;
int dist[N], que[N], hh = 0, tt = -1;
inline void add(int a, int b, int c) { e[++ idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx; }
bool bfs()
{
memset(dist, 0, sizeof dist);
hh = 0, tt = -1;
dist[st] = 1, que[++ tt] = st;
while (hh <= tt)
{
int ver = que[hh ++];
for (int i = h[ver]; ~i; i = ne[i])
{
int to = e[i];
if (!dist[to] && w[i] > 0)
dist[to] = dist[ver] + 1, que[++ tt] = to;
}
}
return dist[ed];
}
int dfs(int ver, int flow)
{
if (ver == ed || !flow) return flow;
for (int &i = cpy[ver]; ~i; i = ne[i])
{
int to = e[i];
if (dist[to] == dist[ver] + 1 && w[i] > 0)
{
int temp = dfs(to, min(flow, w[i]));
if (temp > 0)
{
w[i] -= temp, w[i ^ 1] += temp;
return temp;
}
}
}
return 0;
}
int dinic()
{
int ans = 0, temp = 0;
while (bfs())
{
memcpy(cpy, h, sizeof cpy);
while (temp = dfs(st, INF))
ans += temp;
}
return ans;
}
signed main()
{
// ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
memset(h, -1, sizeof h), idx = -1;
cin >> n >> m >> st >> ed;
while (m --)
{
int a, b, c; cin >> a >> b >> c;
add(a, b, c), add(b, a, 0);
}
cout << dinic() << '\n';
return 0;
}
Reference
P3376 【模板】網路最大流 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)
網路流簡介 - OI Wiki (oi-wiki.org)
最大流 - OI Wiki (oi-wiki.org)