网络流学习笔记
网络流的核心在于建图。建图建出来之后,剩下的基本上只是模板了。
0 参考资料
1 基本定义
一个网络是一张有向图 \((V, E)\),其中每条边都有一个流量 \(c(u,v)\)。一个网络有一个源点 \(S\) 和一个汇点 \(T\)。
网络流满足以下几条性质:
-
流函数 \(f : (x, y) \rightarrow \text{R}\) 是一个二维数对到实数域上的一个映射。
-
容量限制:\(f(x, y) \leq c(x, y)\)。
-
斜对称:\(f(x, y) = -f(y, x)\)。
-
流量守恒:\(\forall i \neq S, i \neq T, \sum f(u, i) = \sum f(i, v)\)。
以下是网络流的相关定义。
-
对于有源汇网格,\(\sum f(S, v) = \sum f(u, T)\),也就是初始流量和结束流量相等。
-
残量网络 \(G_f = (V, E_f)\):\(c(u, v)_f = c(u, v) - f(u, v)\),若 \(c(u, v)_f = 0\),则视为 \((u, v) \not \in E_f\)。
-
增广路:残量网络 \(G_f\) 上的一条从 \(S\) 到 \(T\) 的一条路径。
-
割:将点集划分成 \(A, B\),则称这个划分为一个割。这个割的容量为 \(\sum_{u \in A, v \in B} f(u,v)\)。若 \(u \in A, v \in B\),则称 \((A, B)\) 为割边。
2 网络最大流
著名的网络最大流算法有 Ford-Fulkerson,Dinic 和 Edmonds_Karp。其他我不会,不过多介绍。
对于给定的网络 \((G, F)\),求最大流量。
2.1 增广
2.1.1 增广过程
增广是 FF,EK 和 Dinic 最基本的一个算法。
我们考虑贪心策略。
我们不断地寻找在 \(G_f\) 上的一条增广路。根据贪心策略,我们可以让这条增广路能留满就流满。具体的,我们可以把路径上的所有边地流量加上 \(\min_{(u, v) \in \text{Path}} c_f(u, v)\)。
为了保证贪心的正确性,我们需要支持返回。于是我们在给一条边增加流量 \(v\) 时,还要把这一条边的反向边的 \(c_f(v, u)\) 增加 \(v\),以便在后续的寻找增广路的过程中能撤回这条边的流量。
上述过程被称为增广。
我们发现在一次增广后,原网络的流量只增不减。于是当我们跑完所有增广路之后,原网络的流量便是最大流量。
2.1.2 实现技巧
由于我们在给一条边增加流量 \(v\) 时需要找到这条边的反向边,所以我们可以考虑使用链式前向星来存储边,并且边的编号以 \(0\) 开始。这样,一条编号为 \(\text{id}\) 的边,他的反向边编号为 \(\text{id} \text{xor} 1\)。
2.2 最大流最小割定理
2.2.1 定理内容
一个网络的最大流等于其最小割。
2.2.2 定理证明
结论 1:任意一组流的流量小于等于任意一组割的容量
对于任意一条割边 \((u, v)\),设流经过 \((u, v)\) 的次数为 \(x\),则经过 \((v, u)\) 的次数一定为 \(x - 1\)。否则,这个流不可能从 \(S\) 流到 \(T\)。
由于斜对称性,这条割边的流量就是 \([x - (x - 1)] \times f_{u, v} = f_{u, v}\)。所以经过割边的流量总和为割的容量。
通过容量限制,我们得出总流量小于等于割的容量,证毕。
结论 2:存在一组割使得割的容量与最大流取等
当最大流存在时,残量网络一定不连通。于是剩下的这一个残量网络就形成了一个天然的割,且这个割的容量等于最大流。
综上,证毕。
2.2.3 定理应用
在我们想要求一个网络的最小割时,可以使用最大流算法来间接算出最小割。
根据最小割的证明方法,我们可以发现跑完最大流算法后剩下的残量网络就是一个合法的最小割,非常方便。
2.2.4 构造最小割的技巧
当我们跑完了最大流之后,一般会剩下一个残量网络的最短路,也就是 \(dis_i\)。于是我们就可以方便地按照 \(dis\) 是否存在来划分最小割。
以下是代码实现。
void MinCut(int s, int t) {
for (int i = 1; i <= P; i ++)
if (dis[i] != 2e18)
grp[i] = 1;
else
grp[i] = 0;
}
2.3 Ford-Fulkerson
2.3.1 FF 算法思想
根据增广的方式,我们考虑一种朴素的算法 Ford-Fulkerson。
我们可以对于一个网络 \(G\),随意地选择增广路进行增广。若已经不存在增广路,则现在原网络的流量即为 \(\text{MF}\)。
FF 的时间复杂度为 \(\mathcal{O}(F \times E)\),他和流量相关,所以对于一些流量大的题目无法通过。
2.3.2 FF 算法实现
namespace Ford_Fulkerson {
long long ans, vis[N];
long long dfs(int s, int t, int u, long long res) {
vis[u] = 1;
if (u == t) return res;
for (int i = hd[u]; i > 1; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w && !vis[v]) {
long long flow = dfs(s, t, v, min(res, e[i].w));
if (flow) {
e[i].w -= flow, e[i ^ 1].w += flow;
return flow;
}
}
}
return 0;
}
long long Ford_Fulkerson(int s, int t) {
ans = 0; long long tmp = 0;
while ((tmp = dfs(s, t, s, 2e18))) {
ans += tmp;
for (int i = 1; i <= n; i ++)
vis[i] = 0;
}
return ans;
}
}
2.4 Edmonds_Karp
2.4.1 EK 算法思想
EK 是在 FF 的基础上进行的一个小优化。
我们每次增广的时候,可以使用 bfs 来找到长度最小的一个增广路径进行增广。
于是,我们每次从 \(S\) 出发进行 bfs,然后从 \(T\) 使用路径还原反推回来,以便残量网络的更新。
Edmonds_Karp 算最大流的时间复杂度为 \(\mathcal{O}(nm^2)\)。
2.4.2 EK 代码实现
namespace Edmonds_Karp {
int vis[K], pre[K];
long long dis[K], ans;
bool findPath(int x, int t) {
queue <int> q;
fill(vis + 1, vis + n + 1, 0);
fill(dis + 1, dis + n + 1, 0);
q.push(x); vis[x] = 1; dis[x] = 2e18;
while (!q.empty()) {
int u = q.front(); q.pop();
if (u == t) return true;
for (int i = hd[u]; i > 1; i = e[i].nxt) {
int v = e[i].v;
if (e[i].w == 0) continue;
if (vis[v]) continue;
dis[v] = min(dis[u], e[i].w);
pre[v] = i;
q.push(v);
vis[v] = 1;
}
}
return false;
}
void update(int s, int t) {
int x = t;
while (x != s) {
int v = pre[x];
e[v].w -= dis[t];
e[v ^ 1].w += dis[t];
x = e[v].u;
}
ans += dis[t];
}
long long Edmonds_Karp(int s, int t) {
ans = 0;
while (findPath(s, t))
update(s, t);
return ans;
}
}
2.5 Dinic
2.5.1 Dinic 算法流程
EK 算法在 \(m\) 非常大的时候效率低的离谱。Dinic 有效地解决了这个问题。
Dinic 的核心思想是 分层图 和 选择最短路增广,bfs 分册之后,在 dfs 中使用 多重增广,来降低 EK 的时间复杂度。
给图分册是为了防止在 dfs 过程中流出一个环,导致效率大大降低。
Dinic 有一个非常重要的优化 当前弧优化。我们发现在 dfs 过程中,如果一条边已经被流满,则这条边对当前的 dfs 是无用的。但是并不代表永远没用,它的反向边流量可能在其他 dfs 中起到反悔作用。于是我们记录 \(\text{cur}_i\) 表示 \(i\) 点当前第一条没有流满的边。当前弧优化保证了 Dinic 的时间复杂度。
若不加当前弧优化,Dinic 的复杂度会退化成 EK。由于常数大,实际效果比 EK 还差。
Dinic 的时间复杂度为 \(\mathcal{O}(n^2 m)\)。
实际上容易发现,Dinic 本质是蕴含了 EK 的。
2.5.2 Dinic 注意细节
-
因为需要进行多次分层,所以在每次 bfs 之前需要清空数组。
-
由于一条流满的边并不是永远没用,所以在清空数组时还要把 \(\text{cur}\) 赋值成 \(\text{head}\)。
-
要注意当前弧优化不要写假。
-
注意链式前向星的边要从偶数开始。
2.5.3 Dinic 代码实现
namespace Dinic {
long long ans, dis[N], now[N];
bool Build(int x, int t) {
queue <int> q;
fill(dis + 1, dis + n + 1, 2e18);
q.push(x); dis[x] = 0, now[x] = hd[x];
while (!q.empty()) {
int u = q.front(); q.pop();
if (u == t) return true;
for (int i = hd[u]; i > 1; i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (w != 0 && dis[v] == 2e18) {
dis[v] = dis[u] + 1;
now[v] = hd[v];
q.push(v);
}
}
}
return false;
}
long long update(int x, int t, long long sum) {
if (x == t) return sum;
long long res = 0;
for (int i = now[x]; i > 1 && sum; i = e[i].nxt) {
now[x] = i;
long long v = e[i].v, w = e[i].w;
if (w > 0 && dis[v] == dis[x] + 1) {
long long p = update(v, t, min(sum, w));
if (!p) dis[v] = 2e18;
e[i].w -= p;
e[i ^ 1].w += p;
res += p;
sum -= p;
}
}
return res;
}
long long Dinic(int s, int t) {
ans = 0;
while (Build(s, t))
ans += Dinic::update(s, t, 2e18);
return ans;
}
}
3 网络流 24 题
makabaka.
4 网络流例题
4.1 ABC347G
还算是比较经典了。
首先我们注意到一个性质:\(1 + 3 + \cdots + n = n ^ 2\)。所以我们可以把平方拆开。
然后容易证明 \(a_{i, j}\) 填 \(1\) 一定比填 \(0\) 不劣。
我们可以把 \(a_{i, j}\) 拆成 \(4\) 个点,然后我们想到了最小割。
构造网络:
-
\(a_{i, j, x} \leftarrow a_{i, j, x + 1}\),权重为 \(+\infty\)。
-
若 \(a_{i, j} \neq 0\),\(\forall x \neq a_{i, j} - 1\),\(a_{i, j, x} \rightarrow a_{i, j, x + 1}\),权重为 \(+\infty\)。
-
对于相邻的两个点 \(A, B\),\(\forall x > y, a_{A, x} \rightarrow a_{B, y}\), 权重为 \(2\)。
-
对于相邻的两个点 \(A, B\),\(\forall x, a_{A, x} \rightarrow a_{B, x}\), 权重为 \(1\)。
容易证明这个网络的最小割即为所求。
按照 Dinic 跑最大流,残量网络即为最小割。时间复杂度 \(\mathcal{O}(F\times m) = \mathcal{O}(n^4 \times d^4)\),能过。

浙公网安备 33010602011771号