最大流 / 最小割 / 费用流
最大流 / 最小割 / 费用流
一些定义
流网络: 一个连通图满足 ,其中有源点 汇点
每一条边 有一个非负容量
流:边 的流是一个函数 , ,满足
-
容量限制:
-
斜对称性:
-
流守恒性:若 ,要求
流进 的总流量=离开 的总流量
网络的流:流 定义为
-
即从源点出发的总流表示网络的流。
在最大流问题中,求 到 的最大值流
FF 算法
边的残留容量:
残留网络:流 的残留网络 ,
其中
-
若 则 在残留网络中,且
,所以 也在残留网络中
增广路径:增广路径 是残留网络中 到 的一条简单路径
增广路径的残留容量:
沿着路径增广 :沿着路径的每一条边发送 的流。使得整个网络的流量增加。
因此,最大流问题,转化为若干次增广得到的流的和。
增广时,根据修改流的值与残留容量。
由于斜对称性,要有退流操作,即当正向边增加,需要对反向边减少同样大小的流。
- , ,
为什么要有反向边?——给程序一个反悔的机会
退流操作带来的「抵消」效果使得我们无需担心我们按照「错误」的顺序选择了增广路。
上界是 ,这是最最坏的复杂度。
单次增广 ,增广会使流量增加,增广轮数不超过
常规的方法都是基于 FF
找增广路的思路。
根据不同的实现方式。有 ek
,dinic
,和 sap
正确性
需要最大流最小割定理。。
EK
找增广路最自然的方法: bfs 。
类似最短路的思路在残留网络 上找增广路。
为路径上 的最小值。
沿着路径增广,得到 ,继续在上面操作,直到找不到增广路。
int EK()
{
f = 0;
创建残留网络 G(f);
while (通过 bfs 能找到 G(f) 中存在从 s 到 t 的有向路径)
{
令 P 是在G(f)中从 s 到 t 的一条路径
delta = delta(P)
沿着 P 发送 delta 单位的流
更新 P 上的边的残留容量
f = f + delta;
}
return f; //f是最大流
}
单轮 BFS 增广的时间复杂度是
增广总轮数的上界是 ,这个在 最大流 - OI Wiki 上有严格的分析。
总:
dinic
先对 用 bfs
分层,根据点 到源点 的距离 把图分成若干层。
对于在 得到分层图 ,
其中
-
在残量网络上 BFS ,构造分层图。
-
在分层图上 DFS 找增广路,在回溯时实时更新剩余容量。
分层的作用:给定一个固定的搜索顺序,防止在环中反复流动,减少不必要边的搜索。
这个算法有一个最典型的优化:当前弧优化。
当边 已经增广到极限(边 已无剩余容量或 的后侧已不能继续增广)
则 没必要再向边 增广了。
所以,对于每个结点我们维护器第一条还有用的出边。这就是当前弧优化。
多路增广
我们找到了 到 的一条增广路 ,没必要重新从 开始找。
可能在 上某一点的岔路也能继续增广。
回溯是不必直接
return
,每个点可以流向多条出边,这是基于dfs
一个很自然的实现。
多路增广只是常数优化,而当前弧优化是保证复杂度的一部分。
一次增广 ,最多 次。
时间复杂度: 。
如果直接按照 估计复杂度是不科学的。
往往对于 的题可能也有网络流做法。
可以相信大力出奇迹,更多是要在做题中学会估计复杂度。
ISAP
dinic
还有弊端:每次 dfs
后都要重新 bfs
分层。
第一步同样分层,但略有不同,我们选择在反图上,从 点向 点进行 bfs
。
之后按照层次 dfs
并增广。
不同的是,不用重跑 bfs
来对图上的点重新分层,而是在增广中就完成重分层
当我们发现点 的流量有剩余,修改 。
特别地,若残量网络上 无出边,则
当 时,图上不存在增广路,可终止算法。
-
优化:记录 表示深度为 的点的数量。
在更新 是顺便更新 。若出现 则图出现断层。
无法再找到增广路,直接终止算法,实现时直接将 标为
理论上初始距离标号要用先预处理求得,实践中可以全部设为 0
可以证明:这样做不改变时间复杂度,相当于用一次增广来给 赋值。
推荐 isap
:实现很短,速度很快。
-
补充:正常情况下
isap
是可以用当前弧优化的。 -
但在这一种实现下,需要枚举所有的出边寻找 。
这就变得繁琐了。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 200 + 5, M = 5005;
int n, m;
struct Net {
int St, Ed;
int lst[N], Ecnt;
struct Edge { int to, nxt; LL qz; } e[M << 1];
Net() {
memset(lst, 0, sizeof(lst));
Ecnt = 1;
}
void Ae(int fr, int go, LL vl) {
e[++Ecnt] = (Edge){ go, lst[fr], vl }, lst[fr] = Ecnt;
}
void lk(int u, int v, LL w) {
Ae(u, v, w), Ae(v, u, 0);
}
int dep[N], gap[N];
LL dfs(int u, LL low) {
if (u == Ed) return low;
LL use = 0, rl;
int mn = n - 1;
for (int i = lst[u], v; i; i = e[i].nxt) {
if (!e[i].qz) continue;
if (dep[u] == dep[v = e[i].to] + 1) {
rl = dfs(v, min(e[i].qz, low - use));
e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
if (low == use) return use;
}
mn = min(mn, dep[v]);
}
// use < low, or it won't come here.
--gap[dep[u]];
if (!gap[dep[u]]) dep[St] = n;
++gap[dep[u] = mn + 1];
return use;
}
LL mxfl() {
LL res = 0;
gap[0] = n;
while (dep[St] < n) res += dfs(St, 1e10);
return res;
}
} nt;
int main() {
scanf("%d%d%d%d", &n, &m, &nt.St, &nt.Ed);
for (int i = 1, u, v, w; i <= m; i++) {
scanf("%d%d%d", &u, &v, &w);
nt.lk(u, v, 1ll * w);
}
printf("%lld", nt.mxfl());
}
放一个有弧优化的版本。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 200 + 5, M = 5005;
int n, m;
struct Net {
int St, Ed;
int lst[N], cur[N], Ecnt;
struct Edge { int to, nxt; LL qz; } e[M << 1];
Net() {
memset(lst, 0, sizeof(lst));
Ecnt = 1;
}
void Ae(int fr, int go, LL vl) {
e[++Ecnt] = (Edge){ go, lst[fr], vl }, lst[fr] = Ecnt;
}
void lk(int u, int v, LL w) {
Ae(u, v, w), Ae(v, u, 0);
}
int dep[N], gap[N];
LL dfs(int u, LL low) {
if (u == Ed) return low;
LL use = 0, rl;
int mn = n - 1;
for (int &i = cur[u], v; i; i = e[i].nxt)
if (e[i].qz && dep[u] == dep[v = e[i].to] + 1) {
rl = dfs(v, min(e[i].qz, low - use));
e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
if (low == use) return use;
}
for (int i = lst[u]; i; i = e[i].nxt)
if (e[i].qz) mn = min(mn, dep[e[i].to]);
if (!(--gap[dep[u]])) dep[St] = n;
++gap[dep[u] = mn + 1];
return use;
}
LL mxfl() {
LL res = 0;
gap[0] = n;
while (dep[St] < n) {
memcpy(cur, lst, sizeof(cur));
res += dfs(St, 1e10);
}
return res;
}
} nt;
int main() {
scanf("%d%d%d%d", &n, &m, &nt.St, &nt.Ed);
for (int i = 1, u, v, w; i <= m; i++) {
scanf("%d%d%d", &u, &v, &w);
nt.lk(u, v, 1ll * w);
}
printf("%lld", nt.mxfl());
}
最小割
一些定义
割:把流网络划分成两个部分 ,满足源点 ,汇点 。
割的容量: ,可以用 代表 。
即所有从 到 的边的容量之和。
最大流最小割定理
对于 的一个割 ,
有
既然 是最大流,残量网络中不存在从 到 的增广路,
的出边一定满流。 的入边一定是零流。即
其实有三个可以互相推导的结论。
- 存在一个割满足
- 流 是最大流
- 残量网络上没有增广路径
最小割的方案
求出最小割,将没有满流的边流量设置为
满流的边流量设置为 1
再跑一遍最小割。
之后再学最小割模型的应用。
费用流
定义
引入单位费用 满足斜对称性:
当边 的流量为 需要花费
花费最小的最大流是最小费用最大流。
SSP 算法
名字好像有点陌生,实际上就是一个贪心的思路:沿着单位费用最小的路径增广。
不能直接处理有负环的图。
正确性:归纳法?设图上没有负环,流量为 的最小费用为 ,可得
在 得到的残量网络上找到最短路增广,计算得到 ,则 是最短路长度
假设存在 ,显然是需要经过负环增广。
那既然有负环,我们向负环中增加流量,可不增加 流出流量让 更小,与假设矛盾
所以正确性有保证
复杂度上界是 ,基于最大流的算法改进实现。
改进 EK
直接找最短路上的增广路径即可。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const LL Inf = 1e10;
const int N = 5005, M = 50005;
int n, m, St, Ed, lst[N], Ecnt = 1, inq[N], vis[N], pre[N];
LL dis[N], mxfl, Cost, low[N];
queue<int> Q;
struct Edge { int to, nxt; LL qz, cs; } e[M << 1];
inline void Ae(int fr, int go, int vl, int ad) {
e[++Ecnt] = (Edge){ go, lst[fr], 1ll * vl, 1ll * ad }, lst[fr] = Ecnt;
}
inline bool spfa() {
for (int i = 1; i <= n; i++) dis[i] = Inf, inq[i] = 0, pre[i] = 0;
dis[St] = 0, low[St] = Inf, Q.push(St);
for (int u; !Q.empty(); ) {
u = Q.front(), Q.pop(), inq[u] = 0;
for (int i = lst[u], v; i; i = e[i].nxt) {
if (!e[i].qz) continue;
v = e[i].to;
if (dis[u] + e[i].cs < dis[v]) {
dis[v] = dis[u] + e[i].cs, pre[v] = i;
low[v] = min(low[u], e[i].qz);
if (!inq[v]) Q.push(v), inq[v] = 1;
}
}
}
return dis[Ed] ^ Inf;
}
int main() {
scanf("%d%d%d%d", &n, &m, &St, &Ed);
for (int i = 1, u, v, w, q; i <= m; i++) {
scanf("%d%d%d%d", &u, &v, &w, &q);
Ae(u, v, w, q);
Ae(v, u, 0,-q);
}
while (spfa()) {
LL rl = low[Ed];
for (int u = Ed; u ^ St; u = e[pre[u] ^ 1].to)
e[pre[u]].qz -= rl, e[pre[u] ^ 1].qz += rl;
mxfl += rl, Cost += rl * dis[Ed];
}
printf("%lld %lld", mxfl, Cost);
}
改进 dinic
把原本按照深度分成改为按照最短路分层。
其实 zkw
也提出了这一种做法。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const LL Inf = 1e10;
const int N = 5005, M = 50005;
int n, m, St, Ed, lst[N], Ecnt = 1, inq[N], vis[N];
LL dis[N], mxfl, Cost;
queue<int> Q;
struct Edge { int to, nxt; LL qz, cs; } e[M << 1];
inline void Ae(int fr, int go, int vl, int ad) {
e[++Ecnt] = (Edge){ go, lst[fr], 1ll * vl, 1ll * ad }, lst[fr] = Ecnt;
}
inline bool spfa() {
for (int i = 1; i <= n; i++) dis[i] = Inf, inq[i] = 0;
dis[St] = 0, Q.push(St);
for (int u; !Q.empty(); ) {
u = Q.front(), Q.pop(), inq[u] = 0;
for (int i = lst[u], v; i; i = e[i].nxt) {
if (!e[i].qz) continue;
v = e[i].to;
if (dis[u] + e[i].cs < dis[v]) {
dis[v] = dis[u] + e[i].cs;
if (!inq[v]) Q.push(v), inq[v] = 1;
}
}
}
return dis[Ed] ^ Inf;
}
LL dfs(int u, LL low) {
if (u == Ed) return Cost += dis[Ed] * low, low;
register LL use = 0, rl;
for (int i = lst[u], v; i; i = e[i].nxt)
if (!vis[v = e[i].to] && e[i].qz)
if (dis[u] + e[i].cs == dis[v]) {
vis[v] = 1, rl = dfs(v, min(e[i].qz, low - use)), vis[v] = 0;
e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
if (use == low) return use;
}
return use;
}
int main() {
scanf("%d%d%d%d", &n, &m, &St, &Ed);
for (int i = 1, u, v, w, q; i <= m; i++) {
scanf("%d%d%d%d", &u, &v, &w, &q);
Ae(u, v, w, q);
Ae(v, u, 0,-q);
}
while (spfa()) {
for (int i = 1; i <= n; i++) vis[i] = 0;
vis[St] = 1, mxfl += dfs(St, Inf);
}
printf("%lld %lld", mxfl, Cost);
}
zkw?费用流
问号:应该叫什么。
这是 zkw
提供的一种实现方式,有别于普通的重新跑最短路。
这种方法采用二分图 KM
的重标号思想,如果不知道 KM
是什么也没有关系。
我们考虑跑完最短路后会发生什么:
- 所有点 满足
- 对于每一个 存在一点 使得
而增广之后会破坏什么?
不会是 ,只有可能是满流后导致 不满足,使得不能找到最短路上的增广路。
找增广后,当前流对应割的边集 ,表示到 增广失败了。
找到
所有访问过的点距离标号增加 。这样不会破坏性质 1,
而且至少有一条新的边进入了 的子图
使用范围:不可直接处理有负权的边。
效率问题:在一些图上很快,在一些图上很慢。
-
优点:减少多次访问节点与队列维护。多路增广。
-
缺点:最差情况下,真的一次修改只能让 条边进入最短路径。
反复尝试增广而次次不能增广, 陷入弄巧成拙的境地.
适用于:最终流量较大,而费用取值范围不大的图
慎用与:流量不大,费用不小,增广路还较长,就不适合
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 5005, M = 50005;
const int INF = 0x3f3f3f3f;
struct Edge { int to, nxt, qz, cs; }e[M << 1];
int n, m, S, T, cnt = 1, lst[N], dis[N], Cost, tot;
int vis[N];
inline void Ae(int fr, int go, int vl, int ad) {
e[++cnt] = (Edge){go, lst[fr], vl, ad}, lst[fr] = cnt;
}
inline bool relabel() {
int mn = INF;
for (int u = 1; u <= n; u++) if (vis[u])
for (int i = lst[u], v; i; i = e[i].nxt)
if (!vis[v = e[i].to] && e[i].qz)
mn = min(mn, dis[v] + e[i].cs - dis[u]);
if (mn == INF) return 0;
for (int u = 1; u <= n; u++) if (vis[u]) dis[u] += mn;
return 1;
}
int dfs(int u, int nw) {
if (u == T) return Cost += dis[S] * nw, tot += nw, nw;
register int use = 0, rl;
vis[u] = 1;
for (int i = lst[u], v; i; i = e[i].nxt)
if (!vis[v = e[i].to] && e[i].qz)
if (dis[u] == dis[v] + e[i].cs) {
rl = dfs(v, min(e[i].qz, nw - use));
e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
if (use == nw) return use;
}
return use;
}
int main() {
scanf("%d%d%d%d", &n, &m, &S, &T);
for (int i = 1, u, v, w, q; i <= m; i++) {
scanf("%d%d%d%d", &u, &v, &w, &q);
Ae(u, v, w, q), Ae(v, u, 0, -q);
}
do {
do {
memset(vis, 0, sizeof(vis));
} while(dfs(S, INF));
} while(relabel());
printf("%d %d", tot, Cost);
}
使用 dijkstra
又叫“Primal-Dual 原始对偶算法”??
鉴于 spfa
在某方面的缺点,想用 dijkstra
。
势:给每一个节点赋予一个新的标号 ,叫做“势”只是因为与物理中的势有相似的性质。
在此基础上把边 的长度修改为
证明其可行性,需要三点。
-
在 上跑最短路和在 上跑是等价的。
易得固定对于路径
长度为 ,拆开之后能消掉 ,最后剩下
所以长度等于
对于固定的起点、终点 , 为定值,最短路也自然等价。
-
边权 保持非负。势的初始化?
,整理,
即势能要满足三角不等式。所以先跑一遍
spfa
, 初始化为最初的最短路即可。 -
如何修改势,正确性如何保证?
修改的结论,假设增广后源点 到 的距离是 (修改边权后的距离)
只需给 加上 即可。
若能证明修改后边权保持非负,就是正确的。
-
原有的边。满足
所以用 作为新的势能对原有的边满足条件。
-
在一轮增广后,由于一些 边在增广路上。一定会满足
之后残量网络上会多出一些 边,根据 可以得到
因此新增的边 的边权非负。
-
-
得证,边权全部非负,可以用
dijk
给出一个单路增广的程序。自然可以改成多路增广。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const LL Inf = 1e10;
const int N = 5005, M = 50005;
int n, m, St, Ed, lst[N], Ecnt = 1, inq[N];
LL dis[N], mxfl, Cost, h[N];
queue<int> Q;
struct Edge { int to, nxt; LL qz, cs; } e[M << 1];
inline void Ae(int fr, int go, int vl, int ad) {
e[++Ecnt] = (Edge){ go, lst[fr], 1ll * vl, 1ll * ad }, lst[fr] = Ecnt;
}
void spfa() {
for (int i = 1; i <= n; i++) h[i] = Inf, inq[i] = 0;
h[St] = 0, Q.push(St);
for (int u; !Q.empty(); ) {
u = Q.front(), Q.pop(), inq[u] = 0;
for (int i = lst[u], v; i; i = e[i].nxt) {
v = e[i].to;
if (e[i].qz && h[u] + e[i].cs < h[v]) {
h[v] = h[u] + e[i].cs;
if (!inq[v]) Q.push(v), inq[v] = 1;
}
}
}
}
typedef pair<LL, int> pr;
priority_queue<pr> hp;
int pre[N];
LL low[N];
bool dijk() {
for (int i = 1; i <= n; i++) dis[i] = low[i] = Inf, pre[i] = 0;
dis[St] = 0, low[St] = Inf, hp.push(make_pair(0, St));
while (!hp.empty()) {
int u = hp.top().second;
LL now = -hp.top().first;
hp.pop();
if (now != dis[u]) continue;
for (int i = lst[u], v; i; i = e[i].nxt) {
v = e[i].to;
LL w = e[i].cs + h[u] - h[v];
if (e[i].qz && dis[u] + w < dis[v]) {
dis[v] = dis[u] + w, pre[v] = i;
low[v] = min(low[u], e[i].qz);
hp.push(make_pair(-dis[v], v));
}
}
}
return dis[Ed] != Inf;
}
int main() {
scanf("%d%d%d%d", &n, &m, &St, &Ed);
for (int i = 1, u, v, w, q; i <= m; i++) {
scanf("%d%d%d%d", &u, &v, &w, &q);
Ae(u, v, w, q);
Ae(v, u, 0,-q);
}
spfa();
while (dijk()) {
LL rl = low[Ed];
for (int u = Ed; u ^ St; u = e[pre[u] ^ 1].to)
e[pre[u]].qz -= rl, e[pre[u] ^ 1].qz += rl;
mxfl += rl, Cost += rl * (dis[Ed] + h[Ed] - h[St]);
for (int i = 1; i <= n; i++) h[i] += dis[i];
}
printf("%lld %lld", mxfl, Cost);
}
最后
为什么在费用流这部分放出了 ek
和 dinic
的代码?
因为费用流的时间复杂度很玄学,没有固定哪一种跑得快。
比如流量很小,单路增广可能优于多路增广。
对复杂度的估计是需要练习的。
总结
理解好网络流的基本写法,才能有建模解题的底气。
本文作者:小蒟蒻laf
本文链接:https://www.cnblogs.com/KonjakLAF/p/17232881.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步