目录
前言
Part1 符号及约定
Part2 网络流算法
Part2.1 Ford-Fulkerson 算法
Part2.2 Edmond-Karp 算法
Part2.3 Dinic 算法
Part2.4 ISAP 算法
Part2.5 HLPP 算法
Part2.6 最小(大)费用最大流
Part3 网络流建模和例题
后记
前言
我最近在刷网络流的题,结果啥都不会做……
把一些学习中的心得放到这里,很垃圾,请巨佬们不要吐槽。
本文中添加了一些作者本人自己的见解,如果有误,烦请巨佬们帮忙指出。
本文写作时间:2023.3.30 22.16 ~ ?
Part1 符号及约定
V \textrm{V} V 表示点集, E \textrm{E} E 表示边集。
G = ⟨ V , E ⟩ \textrm{G} = \lang \textrm{V}, \textrm{E} \rang G=⟨V,E⟩ 表示图。
s s s 表示源点, t t t 表示汇点。
u → v u \rarr v u→v 表示存在一条从 u u u 连向 v v v 的边。
u ↛ v u \not \rarr v u→v 表示不存在从 u u u 连向 v v v 的边。
u ⇒ v u \rArr v u⇒v 表示存在一条从 u u u 到 v v v 的路径。
u ⇏ v u \not \rArr v u⇒v 表示不存在一条从 u u u 到 v v v 的路径。
( u , v ) (u, v) (u,v) 表示一条从 u u u 连向 v v v 的边。
( u , v , w ) (u, v, w) (u,v,w) 表示一条从 u u u 连向 v v v,且容量为 w w w 的边。
在最小(大)费用最大流中, ( u , v , w , k ) (u, v, w, k) (u,v,w,k) 表示一条从 u u u 连向 v v v,且容量为 w w w,费用为 k k k 的边。
容量函数 c ( u , v ) \textrm{c}(u, v) c(u,v) 的定义如下:
c ( u , v ) = { 边 ( u , v ) 的容量 u → v 0 u ↛ v \textrm{c}(u, v) = \begin{cases} 边 (u, v) 的容量 & u \rarr v \\ 0 & u \not \rarr v \end{cases} c(u,v)={边(u,v)的容量0u→vu→v
注: 为了方便,假定对于每个顶点 u ∈ V u \in \textrm{V} u∈V 都有 s ⇒ u ⇒ t s \rArr u \rArr t s⇒u⇒t。如果存在点 u u u 使得 s ⇏ u s \not \rArr u s⇒u,那么就增加边 ( s , u , 0 ) (s, u, 0) (s,u,0)。如果存在点 u u u 使得 u ⇏ t u \not \rArr t u⇒t,那么就增加边 ( u , t , 0 ) (u, t, 0) (u,t,0)。
注: 为了方便,假定图中没有边 ( u , v , w 1 ) (u, v, w_1) (u,v,w1) 和 ( v , u , w 2 ) (v, u, w_2) (v,u,w2) ( w 1 ≥ w 2 ∧ w 1 > 0 ∧ w 2 > 0 ) (w_1 \ge w_2 \land w_1 > 0 \land w_2 > 0) (w1≥w2∧w1>0∧w2>0)同时存在,如果存在,那么删去这两条边,且如果 w 1 > w 2 w_1 > w_2 w1>w2 ,就添加一条边 ( u , v , w 1 − w 2 ) (u, v, w_1 - w_2) (u,v,w1−w2)。
流函数 f ( u , v ) \textrm{f}(u, v) f(u,v) 的定义如下:
f ( u , v ) = { 边 ( u , v ) 的流量 u → v − f ( v , u ) v → u 0 u ↛ v ∧ v ↛ u \textrm{f}(u, v) = \begin{cases} 边 (u, v) 的流量 & u \rarr v \\ -\textrm{f}(v, u) & v \rarr u \\ 0 & u \not \rarr v \land v \not \rarr u \end{cases} f(u,v)=⎩ ⎨ ⎧边(u,v)的流量−f(v,u)0u→vv→uu→v∧v→u
流函数满足以下性质:
- 容量限制: ∀ u , v ∈ V , f ( u , v ) ≤ c ( u , v ) \forall u, v \in \textrm{V}, \textrm{f}(u, v) \le \textrm{c}(u, v) ∀u,v∈V,f(u,v)≤c(u,v)。
- 斜对称性: ∀ u , v ∈ V , f ( u , v ) = − f ( v , u ) \forall u, v \in \textrm{V}, \textrm{f}(u, v) = -\textrm{f}(v, u) ∀u,v∈V,f(u,v)=−f(v,u)。
- 流守恒性: ∀ u ∈ ∁ V { s , t } , ∑ x ∈ V f ( u , x ) = 0 \forall u \in \complement_{\textrm{V}}{\{s, t\}}, \sum_{x \in \textrm{V}}{\textrm{f}(u, x)} = 0 ∀u∈∁V{s,t},∑x∈Vf(u,x)=0。(简单来说就是除源点和汇点以外,这个点流进的流量等于它流出的流量)
当流函数 f \textrm{f} f 满足以上三点性质时,我们称它为合法的。
流指一个合法的流函数。
一个流的流量定义如下:
∣ f ∣ = ∑ x ∈ V f ( s , x ) |\textrm{f}| = \sum_{x \in \textrm{V}}\textrm{f}(s, x) ∣f∣=x∈V∑f(s,x)
最大流问题:给出一张图 G = ⟨ V , E ⟩ \textrm{G} = \lang \textrm{V}, \textrm{E} \rang G=⟨V,E⟩,求出 ∣ f ∣ |\textrm{f}| ∣f∣ 的最大值。
残存容量函数的定义如下:
c f ( u , v ) = { c ( u , v ) − f ( u , v ) u → v 0 u ↛ v \textrm{c}_\textrm{f}(u, v) = \begin{cases} \textrm{c}(u, v) - \textrm{f}(u, v) & u \rarr v \\ 0 & u \not \rarr v \end{cases} cf(u,v)={c(u,v)−f(u,v)0u→vu→v
在实际处理中,对于每一条边 ( u , v ) (u, v) (u,v),设它的权值为 c f ( u , v ) \textrm{c}_\textrm{f}(u, v) cf(u,v),并增加一条边 ( v , u , f ( u , v ) ) (v, u, \textrm{f}(u, v)) (v,u,f(u,v))。记这样处理过后的图为 G ′ = ⟨ V , E ′ ⟩ \textrm{G}' = \lang \textrm{V}, \textrm{E}' \rang G′=⟨V,E′⟩,其中 E ′ = { ( u , v , c f ( u , v ) ) ∣ ( u , v ) ∈ E } ∪ { ( v , u , f ( u , v ) ) ∣ ( u , v ) ∈ E } \textrm{E}' = \{(u, v, \textrm{c}_\textrm{f}(u, v)) | (u, v) \in \textrm{E} \} \cup \{(v, u, \textrm{f}(u, v)) | (u, v) \in \textrm{E} \} E′={(u,v,cf(u,v))∣(u,v)∈E}∪{(v,u,f(u,v))∣(u,v)∈E}。(在 Part1.1 中我会说明为什么要这样处理)
残存网络 G f \textrm{G}_\textrm{f} Gf 是在 G ′ \textrm{G}' G′ 中只保留权值大于 0 0 0 的边后产生的图,其中每条边的权值为其对应边的权值。即: G f = ⟨ V , E f ⟩ \textrm{G}_\textrm{f} = \lang \textrm{V}, \textrm{E}_\textrm{f} \rang Gf=⟨V,Ef⟩,其中 E f = { ( u , v , w ) ∣ ( u , v , w ) ∈ E ′ ∧ w > 0 } \textrm{E}_\textrm{f} = \{(u, v, w) | (u, v, w) \in \textrm{E}' \land w > 0 \} Ef={(u,v,w)∣(u,v,w)∈E′∧w>0}。
增广路径(简称增广路)是在 G ′ \textrm{G}' G′ 中的一条从 s s s 到 t t t 的简单路径,该路径上每一条边的权值都不为 0 0 0。
增广路定理:当在 G ′ \textrm{G}' G′ 中不存在增广路时,即当 G f \textrm{G}_\textrm{f} Gf 中 s ⇏ t s \not \rArr t s⇒t 时,此时的 ∣ f ∣ |\textrm{f}| ∣f∣ 为最大流。
maxflow \textrm{maxflow} maxflow 表示最大流, mincut \textrm{mincut} mincut 表示最小割, mincost \textrm{mincost} mincost 表示最小费用最大流中的最小费用, maxcost \textrm{maxcost} maxcost 表示最大费用最大流中的最大费用。
在建模时,如果 G \textrm{G} G 是一张二分图或者类似于二分图,那么把它的左半部分(不包括 s s s)叫做 X \textrm{X} X,把它的右半部分(不包括 t t t)叫做 Y \textrm{Y} Y。
在最小割中,把 s s s 能够到达的节点组成的集合叫做 S \textrm{S} S, s s s 无法到达的节点组成的集合叫做 T \textrm{T} T。
Part2 网络流算法
Part2.1 Ford-Fulkerson 算法
Ford-Fulkerson 算法(简称 FF)是最原始的网络流算法,时间复杂度上限 O ( m f ) O(mf) O(mf)(其中 m m m 是边数, f f f 是最大流的大小)。
Solution
FF 算法的核心思想是不断寻找增广路并计入答案,直到找不到了为止。
但是直接在原图 G \textrm{G} G 里求最大流会出现问题,如下图。
如果我们先找出一条增广路 s → 1 → 2 → t s \rarr 1 \rarr 2 \rarr t s→1→2→t,变成下图:
此时就找不到增广路了,但是实际上该图的最大流是 2 2 2( s → 1 → t s \rarr 1 \rarr t s→1→t 和 s → 2 → t s \rarr 2 \rarr t s→2→t 两条路径)。
所以我们要在 G ′ \textrm{G}' G′ 里求最大流,即给每一条边都建反边,反边的权值是 0 0 0。
边 ( u , v ) (u, v) (u,v) 流过 w w w 的流量,应当将 ( u , v ) (u, v) (u,v) 的权值减去 w w w,把 ( v , u ) (v, u) (v,u) 的权值加上 w w w。
反边的作用可以理解为撤销流量,例如在上面这个例子中,找出增广路 s → 1 → 2 → t s \rarr 1 \rarr 2 \rarr t s→1→2→t 后,还有一条增广路 s → 2 → 1 → t s \rarr 2 \rarr 1 \rarr t s→2→1→t。处理增广路 s → 2 → 1 → t s \rarr 2 \rarr 1 \rarr t s→2→1→t 后, ( 1 , 2 ) (1, 2) (1,2) 这条边的权值是 1 1 1,和原来一样,相当于撤销了 s → 1 → 2 → t s \rarr 1 \rarr 2 \rarr t s→1→2→t 中它流过的流量。
FF 使用 dfs 实现,每次从 s s s 出发,寻找一条增广路并将流量计入答案,每访问一个节点就把这个节点打上标记,下次就不能再进入打过标记的节点,保证 dfs 的时间复杂度是 O ( n + m ) O(n + m) O(n+m)。当不存在从 s s s 到 t t t 的增广路的时候,算法退出并输出答案。
时间复杂度证明:
由于 FF 每执行一次 dfs,答案至少会增加 1 1 1,所以最多会执行 f f f 次最大流。
每一次 dfs 由于标记了是否到达过这个点,时间复杂度是 O ( n + m ) O(n + m) O(n+m) 的。
所以总的时间复杂度是 O ( ( n + m ) ⋅ f ) O((n + m) \cdot f) O((n+m)⋅f),也就是 O ( m f ) O(mf) O(mf)。
证毕。
要注意网络流算法的时间复杂度是玄学,往往跑不满,所以 FF 实际上耗时比 O ( m f ) O(mf) O(mf) 小很多,但是由于它的复杂度和最大流有关,所以还是很慢。
Tip:
如何快速求出一条边的反边的编号呢?
如果我们在建边之前把
num
(即边的总数)设为 1 1 1,且把正边和反边一起建,那么编号为 2 2 2 和编号为 3 3 3 的边就是一对反边,编号为 4 4 4 和编号为 5 5 5 的边也是一对反边……设这条边的编号为
i
,那么这条边的反边的编号就是i ^ 1
。注意: 这样做的前提是一定要在建边之前把
num
(即边的总数)设为 1 1 1,不要漏掉!
Code
下面是P3376 【模板】网络最大流的代码,但是由于太慢而 T 了 2 个点……
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1000, maxm = 40000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans, H[N];
bool vis[N];
struct edg{
ll t, nxt, len;
}E[M * 2];
void add_edg(ll x, ll y, ll w) {
++num;
E[num].t = y;
E[num].nxt = H[x];
E[num].len = w;
H[x] = num;
}
void add_edges(ll x, ll y, ll w) {
add_edg(x, y, w);
add_edg(y, x, 0);
}
ll dfs(ll x, ll y) {
if (x == t) {
return y;
}
vis[x] = 1;
for (ll i = H[x]; i > 0; i = E[i].nxt) {
ll v = E[i].t, w = E[i].len;
if (w > 0 && (!vis[v])) {
ll k = dfs(v, min(w, y));
if (k != -1) {
E[i].len -= k;
E[(i ^ 1)].len += k;
return k;
}
}
}
return -1;
}
void FF() {
ans = 0;
for (ll i = 1; i <= S; ++i) {
vis[i] = 0;
}
ll k = dfs(s, INF);
while (k != -1) {
ans += k;
for (ll i = 1; i <= S; ++i) {
vis[i] = 0;
}
k = dfs(s, INF);
}
}
int main() {
scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
num = 1;
S = n;
for (ll i = 1; i <= m; ++i) {
ll u, v, w;
scanf("%lld%lld%lld", &u, &v, &w);
add_edges(u, v, w);
}
FF();
printf("%lld", ans);
return 0;
}
Part2.2 Edmond-Karp 算法
Edmond-Karp 算法(简称 EK)是 FF 的 bfs 版,时间复杂度上限 O ( n m 2 ) O(nm^2) O(nm2),但是往往比 FF 跑得更快。
(未完待续)
Part2.3 Dinic 算法
Dinic 算法应该可以算最常用的网络流算法,既好写,时间复杂度上限又比 FF 和 EK 低很多,是 O ( n 2 m ) O(n^2 m) O(n2m),跑得比较快。
(未完待续)
Code
下面是P3376 【模板】网络最大流的代码。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1000, maxm = 40000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans, H[N], C[N], de[N];
struct edg{
ll t, nxt, len;
}E[M * 2];
void add_edg(ll x, ll y, ll w) {
++num;
E[num].t = y;
E[num].nxt = H[x];
E[num].len = w;
H[x] = num;
}
void add_edges(ll x, ll y, ll w) {
add_edg(x, y, w);
add_edg(y, x, 0);
}
ll dfs(ll x, ll y) {
if (x == t) {
return y;
}
ll z = y, res = 0;
for (ll i = C[x]; i > 0 && z > 0; i = E[i].nxt) {
C[x] = i;
ll v = E[i].t, w = E[i].len;
if (w > 0 && de[v] != -1 && de[v] == de[x] + 1) {
ll k = dfs(v, min(w, z));
E[i].len -= k;
E[(i ^ 1)].len += k;
z -= k;
res += k;
}
}
return res;
}
bool bfs() {
for (ll i = 1; i <= S; ++i) {
de[i] = -1;
C[i] = H[i];
}
de[s] = 1;
queue<ll> q;
q.push(s);
while (!q.empty()) {
ll x = q.front();
q.pop();
for (ll i = H[x]; i > 0; i = E[i].nxt) {
ll v = E[i].t, w = E[i].len;
if (w > 0 && de[v] == -1) {
de[v] = de[x] + 1;
q.push(v);
}
}
}
return (de[t] != -1);
}
void Dinic() {
ans = 0;
while (bfs()) {
ll k = dfs(s, INF);
ans += k;
}
}
int main() {
scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
num = 1;
S = n;
for (ll i = 1; i <= m; ++i) {
ll u, v, w;
scanf("%lld%lld%lld", &u, &v, &w);
add_edges(u, v, w);
}
Dinic();
printf("%lld", ans);
return 0;
}
Part2.4 ISAP 算法
ISAP 算法的时间复杂度上限和 Dinic 一样,都是 O ( n 2 m ) O(n^2 m) O(n2m) 的,但是往往比 Dinic 跑得更快。
(未完待续)
注: 我的模板因为使用每次通过寻找出边中深度的最小值并更新该点的深度的写法,有点问题,被我 hack 了 2 次,下面是两个 hack 样例:
第 1 个:
input:
7 8 1 7
1 2 1
1 3 1
2 4 1
3 4 1
4 7 1
4 5 1
5 7 1
4 6 2147483647
output:
2
wrong output:
1
第 2 个:
input:
8 9 6 7
6 1 2
1 3 2
3 8 2
8 7 2
1 2 2147483647
2 7 1
6 4 1
4 5 1
5 2 1
output:
3
wrong output:
2
注意:因此,最好写直接把深度加 1 1 1 的写法,既正确性高,又常数小!
Code
下面是P3376 【模板】网络最大流的代码。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 400, maxm = 20000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans, H[N], C[N], de[N], gp[N];
bool fl;
struct edg{
ll t, nxt, len;
}E[M * 2];
void add_edg(ll x, ll y, ll w) {
++num;
E[num].t = y;
E[num].nxt = H[x];
E[num].len = w;
H[x] = num;
}
void add_edges(ll x, ll y, ll w) {
add_edg(x, y, w);
add_edg(y, x, 0);
}
ll dfs(ll x, ll y) {
if (x == t) {
return y;
}
ll z = y, res = 0;
for (ll i = C[x]; i > 0 && z > 0; i = E[i].nxt) {
C[x] = i;
ll v = E[i].t, w = E[i].len;
if (w > 0 && de[v] != -1 && de[v] + 1 == de[x]) {
ll k = dfs(v, min(w, z));
E[i].len -= k;
E[(i ^ 1)].len += k;
z -= k;
res += k;
if (z == 0) {
return res;
}
}
}
--gp[de[x]];
if (gp[de[x]] == 0) {
fl = 0;
}
++de[x];
++gp[de[x]];
return res;
}
void bfs() {
for (ll i = 1; i <= S; ++i) {
de[i] = -1;
}
de[t] = 1;
for (ll i = 1; i <= S + 1; ++i) {
gp[i] = 0;
}
queue<ll> q;
q.push(t);
while (!q.empty()) {
ll x = q.front();
q.pop();
++gp[de[x]];
for (ll i = H[x]; i > 0; i = E[i].nxt) {
ll v = E[i].t;
if (i % 2 == 1 && de[v] == -1) {
de[v] = de[x] + 1;
q.push(v);
}
}
}
}
void ISAP() {
bfs();
ans = 0;
fl = 1;
while (de[s] <= S && fl) {
for (ll i = 1; i <= S; ++i) {
C[i] = H[i];
}
fl = 1;
ll k = dfs(s, INF);
ans += k;
}
}
int main() {
scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
num = 1;
S = n;
for (ll i = 1; i <= m; ++i) {
ll u, v, w;
scanf("%lld%lld%lld", &u, &v, &w);
add_edges(u, v, w);
}
ISAP();
printf("%lld", ans);
return 0;
}
Part2.5 HLPP 算法
最快的网络流算法,时间复杂度 O ( n 2 m ) O(n^2 \sqrt{m}) O(n2m),我还不会……
(未完待续)
Part2.6 最小(大)费用最大流
最小(大)费用最大流是网络最大流的拓展,每一条边除了容量之外,还增加了一个费用,整张网络的总费用为每一条边的流量乘费用的和。在保证流量最大的前提下,要求出最小(大)的费用。
最大费用最大流跑最长路即可。
(未完待续)
Code
下面是P3381 【模板】最小费用最大流的代码。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 20000, maxm = 400000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans1, ans2, H[N], C[N], dis[N];
bool b[N], vis[N];
struct edg{
ll t, nxt, len, k;
}E[M * 2];
void add_edg(ll x, ll y, ll w, ll k) {
++num;
E[num].t = y;
E[num].nxt = H[x];
E[num].len = w;
E[num].k = k;
H[x] = num;
}
void add_edges(ll x, ll y, ll w, ll k) {
add_edg(x, y, w, k);
add_edg(y, x, 0, -k);
}
ll dfs(ll x, ll y) {
if (x == t) {
return y;
}
vis[x] = 1;
ll z = y, res = 0;
for (ll i = C[x]; i > 0 && z > 0; i = E[i].nxt) {
C[x] = i;
ll v = E[i].t, w = E[i].len, k = E[i].k;
if (w > 0 && (!vis[v]) && dis[v] == dis[x] + k) {
ll k = dfs(v, min(w, z));
E[i].len -= k;
E[(i ^ 1)].len += k;
z -= k;
res += k;
}
}
return res;
}
bool bfs() {
for (ll i = 1; i <= S; ++i) {
C[i] = H[i];
dis[i] = INF;
b[i] = 0;
vis[i] = 0;
}
dis[s] = 0;
bool z = 0;
queue<ll> q;
q.push(s);
b[s] = 1;
while (!q.empty()) {
ll x = q.front();
q.pop();
if (x == t) {
z = 1;
}
b[x] = 1;
for (ll i = H[x]; i > 0; i = E[i].nxt) {
ll v = E[i].t, w = E[i].len, k = E[i].k;
if (w > 0 && dis[x] + k < dis[v]) {
dis[v] = dis[x] + k;
if (!b[v]) {
b[v] = 1;
q.push(v);
}
}
}
b[x] = 0;
}
return z;
}
void Dinic() {
ans1 = 0;
ans2 = 0;
while (bfs()) {
ll k = dfs(s, INF);
ans1 += k;
ans2 += (k * dis[t]);
}
}
int main() {
scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
num = 1;
S = n;
for (ll i = 1; i <= m; ++i) {
ll u, v, w, k;
scanf("%lld%lld%lld%lld", &u, &v, &w, &k);
add_edges(u, v, w, k);
}
Dinic();
printf("%lld %lld", ans1, ans2);
return 0;
}
Part3 网络流建模和例题
后记
本文更新记录
1.0 版:2023.3.30
最初版本。
1.1 版:2023.3.31
发现 ISAP 的模板有 2 个错误,进行了改正,并放上了 2 个 hack 样例。
1.2 版:2023.4.5
写了 Part1 和 Part2.1。