网络流从源点到汇点
阅读方式:PDF(强烈推荐)
最大流、最小割、费用流
定义
网络(Network): 有向图
可行流(Flow):从边集
- 容量限制:
\forall (u,v)\in E ,0\le f(u,v)\le c(u,v) 。每条边的流量不超过每条边的容量。 - 流量守恒:
\forall u\in V\setminus \set{S,T} ,\sum_{(v,u)\in E} f(v,u)=\sum_{(u,v)\in E} f(u,v) 。除源点和汇点外,流入每个点的流量等于流出每个点的流量。 - 斜对称性:
\forall (u,v)\in E ,f(u,v)=-f(u,v) 。从u\to v 流f(u,v) 的流量,等价于从v\to u 流-f(u,v) 的流量。
如图所示,网络可以想象成若干个过水装置,其中
定义可行流的流量为
相等的原因很简单,还是从出入水装置的角度考虑。因为从
残量网络(Residual Network):残量网络
⚠️注:由于上文所提到的斜对称性,
f(u,v) 存在负值,所以E_f 可能存在E 中的反向边。
割(cut): 把
最大流、最小割(maxflow, mincut): 最大流指最大的割的流量,最小割指最小的割的容量。
最小费用最大流(mincost, maxflow):费用流指每条边不仅有容量还有费用,最终的费用定义为每条边的费用
如何求解最大流、最小割便是本节所要讨论的问题,接下来探讨最大流和最小割之间的关系。
最大流最小割定理
若笔者记得没错,定理的内容本身是很长的,证明也较为复杂(好像有
核心: 最大流等于最小割。
证明: 相信大家都不喜欢枯燥无味的数学证明,这里从直观上证明一下。
-
任意可行流的流量
\le 该可行流的任意割的容量:因为从出入水装置的角度思考,割相当于将管道劈断,想象一下可行流在该横截面流过的水,必然不能超出所有劈断管道的容量之和,否则就盛不下了。 -
存在可行流,满足某个割等于该可行流的流量:该可行流需要满足残量网络上
s 无法到达t 。选取在残量网络s 所能到达的所有点作为S ,则T=V\setminus S 。这样,\forall u\in S,v\in T, c_f(u,v)=c(u,v)-f(u,v)=0 即,
f(u,v)=c(u,v) 。所以,c(S,T)=\sum_{u\in S}\sum_{v\in T}f(u,v)=可行流的流量 。
综上,可以得出最大流等于最小割。
Ford–Fulkerson 增广
有的读者可能问,为什么要定义残量网络?有的读者可能问,为什么要考虑
直接的求解最大流的思路是每次在网络中找到一条还能流
很可惜,这是错的!如下图所示
如果最初选择了
所以,我们需要反悔!所以,这就是加入反向边的原因,流反向边等价于将之前流的退回去,这个操作称之为推流。
于是,我们得到了一个求解最大流的方法,每次找出一条增广路,对应更新边的流量和反向边的流量。不过,这样做的时间复杂度是
这个也叫 FF 算法,时间复杂度是伪多项式的,这很不好,接下来探索一些多项式做法。
Edmonds-Karp
注意到,每次增广路的条件是任意的。也就是说,一个优化方向是限制每次选择增广路的条件,从而获得更优的复杂度。
EK 算法每次增广选择最短路径(即边数最小的路径),而如果这样做增广的次数便是
证明: 每次增广时,必然会在残量网络上删除一条边。那么,对于边
根据传递性,不难得到
也就是说,对于每条边,至多删除
【参考代码】
i64 EK(int s, int t) {
i64 res = 0;
while (1) {
memset(flow, -1, sizeof flow);
tt = 0, q[ ++ tt] = s, flow[s] = INT_MAX;
for (int i = 1; i <= tt; i ++) {
int u = q[i];
for (int j = h[u]; ~j; j = ne[j]) {
int v = e[j];
if (~flow[v] || !f[j]) continue;
flow[v] = min(flow[u], f[j]);
q[ ++ tt] = v, pre[v] = j;
}
}
if (flow[t] == -1) break;
int cur = t, i;
while (cur != s) {
i = pre[cur], f[i] -= flow[t];
f[i ^ 1] += flow[t], cur = e[i ^ 1];
}
res += flow[t];
}
return res;
}
Dinic
EK 算法每次增广一条增广路,而 BFS 可以同时处理出多条最短路,形成最短路 DAG。在这张最短路 DAG 上,可以同时增广多条增广路,所以只需要找出最大的可能流的流量即可。
这个可以 DFS 求解,对于每个点存储前面最小的剩余容量,每次枚举邻边 DFS(v, min(lim - flow, w)))。
当前弧优化:当 DFS 从
这样,增广的次数是
综上,时间复杂度为
【参考代码】
bool bfs() {
memset(dist, -1, sizeof dist);
tt = 0, q[ ++ tt] = s, dist[s] = 0;
for (int i = 1; i <= tt; i ++) {
int u = q[i];
cur[u] = h[u];
for (int j = h[u]; ~j; j = ne[j]) {
int v = e[j];
if (~dist[v] || !f[j]) continue;
dist[v] = dist[u] + 1;
q[ ++ tt] = v;
}
}
return ~dist[t];
}
i64 dfs(int u, i64 lim) {
if (u == t) return lim;
i64 flow = 0;
for (int i = cur[u]; ~i && flow <= lim; i = ne[i]) {
cur[u] = i;
int v = e[i];
if (dist[v] == dist[u] + 1 && f[i]) {
i64 t = dfs(v, min(lim - flow, (i64)f[i]));
flow += t, f[i] -= t, f[i ^ 1] += t;
}
}
return flow;
}
i64 dinic() {
i64 res = 0;
while (bfs()) res += dfs(s, LONG_LONG_MAX);
return res;
}
最小费用最大流
如今有费用,那么直观上的方式是每次增广费用和最小的路径,这样才是最优的。事实也确实如此,证明略,感兴趣的读者可以自行上网查找。
所以,每次只需要将 EK 中的 BFS 替换成 SPFA(注意有负边权,因为需要考虑反向边),时间复杂度
该算法称之为 Successive Shortest Path,简称 SSP,业界公约。实现是简单的,参考代码略。
上下界网络流
无源汇上下界可行流
问题描述: 给定一张
如果有下界怎么办?考虑强制选择下界,容量变成上界
于是,考虑进行补流。建立超级源点
由于
【参考代码】
const int maxn = 205;
int n, m;
int diff[maxn];
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> m;
maxflow<int> mf(n + 1);
std::vector<array<int, 3>> edge;
for (int i = 1; i <= m; i ++) {
int u, v, lo, hi;
cin >> u >> v >> lo >> hi;
diff[u] += lo, diff[v] -= lo;
mf.add(u, v, hi - lo);
edge.push_back({u, v, lo});
}
for (int i = 1; i <= n; i ++)
if (diff[i] > 0) mf.add(i, n + 1, diff[i]);
else if (diff[i] < 0) mf.add(0, i, -diff[i]);
mf.dinic(0, n + 1);
for (int i = 2 * m; i < mf.idx; i += 2)
if (mf.f[i]) {
cout << "NO" << endl;
return 0;
}
cout << "YES" << endl;
int id = 1;
for (auto [u, v, w] : edge) {
cout << mf.f[id] + w << endl;
id += 2;
}
return 0;
}
有源汇上下界最大流
有源汇上下界可行流是简单的,这里直接讨论最大流的求解。沿用
考虑如何将
在任意可行流上,继续跑 dinic 仍然可以求解出最大流,因为相当于每条边
【参考代码】
const int maxn = 205;
int n, m, s, t;
int diff[maxn];
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> m >> s >> t;
maxflow<i64> mf(n + 1);
for (int i = 1; i <= m; i ++) {
int u, v, lo, hi;
cin >> u >> v >> lo >> hi;
mf.add(u, v, hi - lo);
diff[u] += lo, diff[v] -= lo;
}
mf.add(t, s, INT_MAX);
for (int i = 1; i <= n; i ++)
if (diff[i] > 0) mf.add(i, n + 1, diff[i]);
else if (diff[i] < 0) mf.add(0, i, -diff[i]);
mf.dinic(0, n + 1);
for (int i = 2 * m + 2; i < mf.idx; i += 2)
if (mf.f[i]) {
cout << "please go home to sleep" << endl;
return 0;
}
cout << mf.dinic(s, t) << endl;
return 0;
}
有源汇上下界最小流
最小流与最大流的求解方式极为类似,核心思想不变:先求解可行流,将
第一步与最大流完全一样,这里不再赘述。考虑现在求解出一张平衡
注意:在求
【参考代码】
const int maxn = 50005;
int n, m, s, t;
i64 diff[maxn];
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> m >> s >> t;
maxflow<i64> mf(n + 1);
for (int i = 1; i <= m; i ++) {
int u, v, lo, hi;
cin >> u >> v >> lo >> hi;
mf.add(u, v, hi - lo);
diff[u] += lo, diff[v] -= lo;
}
mf.add(t, s, INT_MAX);
for (int i = 1; i <= n; i ++)
if (diff[i] > 0) mf.add(i, n + 1, diff[i]);
else if (diff[i] < 0) mf.add(0, i, -diff[i]);
mf.dinic(0, n + 1);
i64 res = mf.f[2 * m + 1];
for (int i = 2 * m + 2; i < mf.idx; i += 2)
if (mf.f[i]) {
cout << "please go home to sleep" << endl;
return 0;
}
mf.f[2 * m] = mf.f[2 * m + 1] = 0;
cout << res - mf.dinic(t, s) << endl;
return 0;
}