「笔记」网络流乱记
不会咕!
个人觉得网络流学混了,暂停一下,搞懂再来……不会咕的,谢谢支持!
基本概念
下面是一些概念,理解即可,不需强记。
网络流图
一个网络流图是一个没有自环的有向连通图,满足:
- 只有一个入度为 \(0\) 的点 \(S\),称为源点;
- 只有一个出度为 \(0\) 的点 \(T\),称为汇点;
- 每条边(弧) \((i,j)\) 有一个非负实数权值 \(c_{i,j}\),称为该边的容量
容许流
容许流,又叫可行流.
在网络流图中,对于每条边 \(e=(i,j)\) 给定实数 \(f_e\), 如果满足:
-
每一条边的容量 \(f_e\le c_e\);
-
流量平衡:对于任意 \(x\ne S, T\),\(\sum\limits_{e=(x,i)}f_e=\sum\limits_{e=(i,x)}f_e\);
-
对于源点 \(S\) 和汇点 \(T\) 有:\(\sum\limits_{e=(S,i)}f_e=\sum\limits_{e=(i,T)}f_e=W\)
即源点流进去多少,汇点就会流出多少,不会有流量的损失.
则称这一组 \(f\) 为该网络的一个容许流,\(W\)称为他的流量。
最大流
一个容量 \(W\) 最大的容许流 。
可增广路
可增广路,又叫可改进路.
介绍这个概念之前,先看看几个别的概念:
给定一个容许流 \(f\) 。
- 若对于一条边 \((i,j)\) 有 \(f_{i,j}=c_{i,j}\),则称 \((i,j)\) 为饱和弧,否则称非饱和弧;
- 若对于一条边 \((i,j)\) 有 \(f_{i,j}=0\),则称 \((i,j)\) 为零流弧,否则称非零流弧;
- 定义一条道路 \(P\),起点是 \(S\),终点是 \(T\)。把 \(P\) 上所有与 \(P\) 方向一致的弧称为正向弧 ,正向弧的全体记为 \(P^+\);把 \(P\) 上所有与 \(P\) 方向相悖的弧称为反向弧,反向弧的全体记为 \(P^-\)
给定一个容许流 \(f\),\(P\) 是从 \(S\) 到 \(T\) 的一条道路,如果满足:
- \(f_{i,j}\) 是非饱和弧,且 \((i,j)\in P^+\);
- 或 \(f_{i,j}\) 是非零流弧,且 \((i,j)\in P^-\)
那么就称 \(P\) 是 \(f\) 的一条可增广路,之所以称为“可增广路”,是因为可改进路上弧的流量通过一定的规则修改,可以使整个流量放大。
最大流算法
基本思路:不断尝试寻找增流路径,增加容许流的流量,直到无法增加为止,此过程称为增广过程
Ford-Fulkerson 标号方法
简介
\(\text{Ford-Fulkerson}\),一种迭代方法,先对图中所有顶点的流清零(此时网络流大小也为 \(0\))。在每次迭代中,通过寻找一条增广路径来增加流的值,一直迭代到无法再找到增广路径为止。
之所以称为方法,是因为它并没有对算法步骤中寻找增广路的步骤提出明确的寻找方法,在后面的学习中,我们会发现寻找增广路的效率是判断各个算法优劣的主要依据。
残留网络
剩余的容量+反向平衡的流量共同构成了残留网络。
残留容量:对于一条边 \((i,j)\),在不超过容量 \(c_{i,j}\) 的条件下,从 \(i\) 到 \(j\) 之间可以压入的额外网络流量,就是 \((i,j)\) 的残留容量,公式定义是 \(v_{i,j}=c_{i,j}-f_{i.j}\)。
而 残留网络 就是 残留容量 组成的网络
所以这个定义有什么用呢?其实用处是很大的。
-
比如有下面这样一个图,起点为 \(1\) 号点,终点为 \(4\) 号点;
-
在过程中我们可能会找到下面这种情况;
-
然后就会发现:我们没法再走了,但真正的最大流不是这个,而是下图这种情况,
最大流量为 \(4\), 那么应该如何解决这样的问题呢,这个时候就要用到残留网络了.
-
在建图时,建正向边的同时,建一条流量为 \(0\) 的方向边.
在找最大流的同时,如果搜到了一条合法路径.
就让正向边的流量减去这次搜索的流量,并让反向边加上这次搜索的流量.
这样就能避免上面的问题出现了,如下图
这样就相当于 \(3.\) 中的情况了,在搜 \(2\sim 3\) 的边时,反向边加了流量 \(2\),下次从粉色路线开始时,可以从 \(3\) 号走到 \(2\) 号,这样其实就抵消了这条边的使用,最后得出的最大流也是 \(4\)。
具体步骤
- 初始化网络中每条边的容量,对于一条边 \((i,j)\), \(c_{i,j}\) 为这条边的容量,\(c_{j,i}\)为 \(0\),初始化最大流为 \(0\);
- 在残留网络中找到一条从 \(S\) 到 \(T\) 的增广路 \(P\),如果能找到赚到步骤 \(3\),否则转到步骤 \(5\);
- 在增广路中找到“瓶颈边”(即容量最小的边),记下容量 \(x\),加到最大流中;
- 将增广路中所有边的容量减去 \(x\),相对的反向边的容量加上 \(x\),构成新的残留网络,转步骤 \(2\);
- 得到最大流并退出;
正确性
首先每条边的\(v_{i,j}\)不会变成负数(即\(f_{i,j}\le c_{i,j}\)),其次,每次增广时,一个点的入流量和出流量总是同时变化相同的值,总满足对于每个点的流量平衡,所以这个方法是正确的。
EK算法
简介
\(\text{EK}\) 算法,全称 \(\text{Edmod-Karp}\) 算法,是一种效率比较低下的 \(\text{FF}\) 方法的实现方法,每次沿着一条最短(边数最少)的增广路进行增广,可以用 \(\text{BFS}\) 来写。
由于效率较低我懒,不做具体说明
复杂度
\(\text{EK}\) 算法的复杂度与边的容量无关,且增广路的条数不超过\(\frac{m(n+2)}{2}\),因此时间复杂度上限为\(O(nm^2)\),而且一般远远达不到这个复杂度(但是我不会)
代码
此代码为 洛谷 P3376 【模板】网络最大流 的代码,比较坑的是这道题里面有重边
/*
Author:loceaner
*/
#include <queue>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int A = 1e3 + 11;
const int B = 1e6 + 11;
const int mod = 1e9 + 7;
const int inf = 1e18;
inline int read() {
char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}
//邻接矩阵存图,因为能够用EK算法跑过去的必然点也不多……
queue <int> Q;
int n, m, s, t, res[A], f[A][A], c[A][A], pre[A];
int EK(int S, int T) {
int ans = 0;
while (1) {
memset(res, 0, sizeof(res));
memset(pre, -1, sizeof(pre));
res[S] = inf, Q.push(S);
//源点的残留网络要无限大,否则找增广路会出错
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 1; i <= n; i++) {
if (!res[i] && f[x][i] < c[x][i]) {
Q.push(i), pre[i] = x;
res[i] = min(c[x][i] - f[x][i], res[x]);
//如果有增广路,那么res[t]就是增广路的最小权值
}
}
}
if (res[T] == 0) break; //找不到增广路,退出
int k = T;
//将路径上的边进行流量加减操作
while (pre[k] != -1) {
f[pre[k]][k] += res[T];
//正向边加上新的流量
f[k][pre[k]] -= res[T];
//反向边减去新的流量,作用是提供反悔机会
k = pre[k];
}
ans += res[T];
}
return ans;
}
signed main() {
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; i++) {
int x = read(), y = read(), w = read();
c[x][y] += w;
}
cout << EK(s, t) << '\n';
return 0;
}
SAP算法
简介
\(\text{SAP}\) 算法是采用了距离标号的方法来求最短的增广路,而举例标号的思想来自于 \(\text{push-relable}\) (压入重标记)类算法。
所谓距离标号,就是某个点到汇点的最少的边的数量(另外一种说法是从源点到该点的最少的边的数量,无本质区别)。
原理
设 \(i\) 点的标号为 \(d_i\),如果将满足 \(d_i=d_j+1\) 的弧叫做允许弧,且增广时只走允许弧,就可以达到“怎么走都是最短路”的效果。
每个点的初始标号可以在一开始用一次从汇点沿所有反向边的 \(\text{BFS}\) 求出,实际操作时可以初始化全部点的距离标号为 \(0\),问题就是如何在增广过程中维护这个距离标号。
维护距离标号的方法
当找增广路过程中发现某点出发没有允许弧时,将这个点的距离标号设为由它出发的所有弧的终点的距离标号的最小值加一。
由于距离标号的存在,“怎么走都是最短路”,所以就可以采用 \(\text{DFS}\) 找增广路,用一个栈保存当前路径的弧即可。
当某个点的距离标号被改变时,栈中指向它的那条弧肯定已经不是允许弧了,所以就让它出栈,并继续用栈顶的弧的端点增广。
优化
-
为了使每次找增广路的时间变成均摊 \(点数O(\text{点数})\),一个重要的优化就是对于每个点保存当前弧:
初始时当前弧是邻接表的第一条弧;
在邻接表查找时,从当前弧开始查找,找到了一条允许弧,就把这条弧设置为当前弧;
在改变距离标号时,把当前弧重新设置为邻接表的第一条弧,这里还有一个常数优化:在改变距离标号时把当前弧设为那条提供了最小标号的弧。当前弧的写法之所以正确就是因为任何时候我们都能保证在邻接表当前弧的前面一定不存在允许弧。 -
还有一个常数优化是在每次找到路径并增广完毕后,不要将路径中所有的点退栈,而是只将瓶颈边以及它之后的边退栈,这是借鉴了 \(\text{Dinic}\) 算法的思想。
注意任何时候待增广的“当前点”都应该是栈顶的点的终点。
这只是一个常数优化,由于当前边结构的存在,我们肯定可以在 \(O(n)\) 的时间内复原路径中瓶颈边之前的所有边。
代码
此代码为无优化、递归版本的代码(未经测试,谨慎使用)
//x 为当前点,flow 为能流向 x 的流量
//返回值为实际流向 x 的流量
int dfs(int x, int flow) {
if (x == t) return flow;
int sum = 0; //x 已经流出去的流量
for (int i = 1; i <= n/*点*/; i++) {
if (g[x][i] && d[x] == d[i] + 1) {
int tmp = dfs(i, min(g[x][i], flow - sum));
g[x][i] -= tmp, g[i][x] += tmp;
sum += tmp;
if (sum == flow) return sum;
}
}
if (flag == 1) return sum;
cnt[d[x]]--; //cnt 表示 d 等级的点有多少个
if (!cnt[d[x]]) flag = 1;
//这个等级的点没有了,就不能再搜了
d[x]++, cnt[d[x]]++;
return sum;
}
Dinic算法
最小费用最大流
题……
写在最后
感谢 ACM 大神 dengsiyu 和 XLightGod 大神的讲课!