最大流 / 最小割 / 费用流
最大流 / 最小割 / 费用流
一些定义
流网络:\(G=(V,E)\) 一个连通图满足 \(|E|\ge |V|-1\) ,其中有源点 \(S\) 汇点 \(T\)
每一条边 \((u,v)\) 有一个非负容量 \(c(u,v)\ge 0\)
流:边 \((u,v)\) 的流是一个函数 \(f(u,v)\) , \(\forall u,v\in V\) ,满足
-
容量限制: \(f(u,v)\le c(u,v)\)
-
斜对称性: \(f(u,v)=-f(v,u)\)
-
流守恒性:若 \(u\notin\{S,T\}\) ,要求 \(\sum_v f(u,v)=\sum_w f(w,u)\)
流进 \(u\) 的总流量=离开 \(u\) 的总流量
网络的流:流 \(f\) 定义为 \(f=\sum_{v\in V} f(S,v)\)
-
即从源点出发的总流表示网络的流。
在最大流问题中,求 \(S\) 到 \(T\) 的最大值流
FF 算法
边的残留容量: \(r(u,v)=c(u,v)-f(u,v)\)
残留网络:流 \(f\) 的残留网络 \(G_f=(V,E_f)\) ,
其中 \(E_f=\{(u,v)\mid u,v\in V\and r(u,v)>0\}\)
-
若 \(0<f(u,v)<c(u,v)\) 则 \((u,v)\) 在残留网络中,且
\(r(v,u)=c(v,u)-f(v,u)>0\) ,所以 \((v,u)\) 也在残留网络中
增广路径:增广路径 \(P\) 是残留网络中 \(S\) 到 \(T\) 的一条简单路径
增广路径的残留容量: \(\delta(P)=\min\{r(u,v)\mid(u,v)\in P\}\)
沿着路径增广 :沿着路径的每一条边发送 \(\delta(P)\) 的流。使得整个网络的流量增加。
因此,最大流问题,转化为若干次增广得到的流的和。
增广时,根据修改流的值与残留容量。
由于斜对称性,要有退流操作,即当正向边增加,需要对反向边减少同样大小的流。
- \(f=f+\delta(P)\)
- \(\forall(u,v)\in P\) ,\(r(u,v)\leftarrow r(u,v)-\delta(P)\) ,\(r(v,u)\leftarrow r(v,u)+\delta(P)\)
为什么要有反向边?——给程序一个反悔的机会
退流操作带来的「抵消」效果使得我们无需担心我们按照「错误」的顺序选择了增广路。
上界是 \(O(|E||f|)\) ,这是最最坏的复杂度。
单次增广 \(O(|E|)\) ,增广会使流量增加,增广轮数不超过 \(|f|\)
常规的方法都是基于 FF
找增广路的思路。
根据不同的实现方式。有 ek
,dinic
,和 sap
正确性
需要最大流最小割定理。。
EK
找增广路最自然的方法: bfs 。
类似最短路的思路在残留网络 \(G_f\) 上找增广路。
\(\delta(P)\) 为路径上 \(r(u,v)\) 的最小值。
沿着路径增广,得到 \(G_f'\) ,继续在上面操作,直到找不到增广路。
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 增广的时间复杂度是 \(O(|E|)\)
增广总轮数的上界是 \(O(|V||E|)\) ,这个在 最大流 - OI Wiki 上有严格的分析。
总: \(O(nm^2)\)
dinic
先对 \(G_f\) 用 bfs
分层,根据点 \(u\) 到源点 \(S\) 的距离 \(d(u)\) 把图分成若干层。
对于在 \(G_f=(v,E_f)\) 得到分层图 \(G_L=(V,E_L)\) ,
其中 \(E_L=\{(u,v)\mid(u,v)\in E_f,d_v=d_u+1\}\)
-
在残量网络上 BFS ,构造分层图。
-
在分层图上 DFS 找增广路,在回溯时实时更新剩余容量。
分层的作用:给定一个固定的搜索顺序,防止在环中反复流动,减少不必要边的搜索。
这个算法有一个最典型的优化:当前弧优化。
当边 \((u,v)\) 已经增广到极限(边 \((u,v)\) 已无剩余容量或 \(u\) 的后侧已不能继续增广)
则 \(u\) 没必要再向边 \((u,v)\) 增广了。
所以,对于每个结点我们维护器第一条还有用的出边。这就是当前弧优化。
多路增广
我们找到了 \(S\) 到 \(T\) 的一条增广路 \(P\) ,没必要重新从 \(S\) 开始找。
可能在 \(P\) 上某一点的岔路也能继续增广。
回溯是不必直接
return
,每个点可以流向多条出边,这是基于dfs
一个很自然的实现。
多路增广只是常数优化,而当前弧优化是保证复杂度的一部分。
一次增广 \(O(nm)\) ,最多 \(O(n)\) 次。
时间复杂度: \(O(n^2 m)\) 。
如果直接按照 \(O(n^2 m)\) 估计复杂度是不科学的。
往往对于 \(n\le 10^5\) 的题可能也有网络流做法。
可以相信大力出奇迹,更多是要在做题中学会估计复杂度。
ISAP
dinic
还有弊端:每次 dfs
后都要重新 bfs
分层。
第一步同样分层,但略有不同,我们选择在反图上,从 \(t\) 点向 \(s\) 点进行 bfs
。
之后按照层次 dfs
并增广。
不同的是,不用重跑 bfs
来对图上的点重新分层,而是在增广中就完成重分层
当我们发现点 \(u\) 的流量有剩余,修改 \(d_u=\min_v d_v+1\) 。
特别地,若残量网络上 \(u\) 无出边,则 \(d_u=n\)
当 \(d_S \geq n\) 时,图上不存在增广路,可终止算法。
-
\(gap\) 优化:记录 \(gap_i\) 表示深度为 \(i\) 的点的数量。
在更新 \(d_u\) 是顺便更新 \(gap\) 。若出现 \(gap_i=0\) 则图出现断层。
无法再找到增广路,直接终止算法,实现时直接将 \(d_S\) 标为 \(n\)
理论上初始距离标号要用先预处理求得,实践中可以全部设为 0
可以证明:这样做不改变时间复杂度,相当于用一次增广来给 \(dep\) 赋值。
推荐 isap
:实现很短,速度很快。
-
补充:正常情况下
isap
是可以用当前弧优化的。 -
但在这一种实现下,需要枚举所有的出边寻找 \(\min dep_v\) 。
这就变得繁琐了。
#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());
}
最小割
一些定义
割:把流网络划分成两个部分 \(S,T\) ,满足源点 \(s\in S\) ,汇点 \(t\in T\) 。
割的容量: \(c(S,T)=\sum_{u\in S}\sum_{v\in T}c(u,v)\) ,可以用 \(c(s,t)\) 代表 \(C(S,T)\) 。
即所有从 \(S\) 到 \(T\) 的边的容量之和。
最大流最小割定理
\(f(s,t)_{max}=c(s,t)_{min}\)
对于 \(f(s,t)\) 的一个割 \(c(s,t)\) ,
有
既然 \(f(s,t)\) 是最大流,残量网络中不存在从 \(S\) 到 \(T\) 的增广路,
\(S\) 的出边一定满流。 \(S\) 的入边一定是零流。即 \(\sum_{u\in S}\sum_{v\in T}f(v,u) = 0\)
其实有三个可以互相推导的结论。
- 存在一个割满足 \(f(s,t)=c(s,t)\)
- 流 \(f\) 是最大流
- 残量网络上没有增广路径
最小割的方案
求出最小割,将没有满流的边流量设置为 \(\infin\)
满流的边流量设置为 1
再跑一遍最小割。
之后再学最小割模型的应用。
费用流
定义
引入单位费用 \(w(u,v)\) 满足斜对称性: \(w(u,v)=-w(v,u)\)
当边 \((u,v)\) 的流量为 \(f(u,v)\) 需要花费 \(f(u,v)\times w(u,v)\)
花费最小的最大流是最小费用最大流。
SSP 算法
名字好像有点陌生,实际上就是一个贪心的思路:沿着单位费用最小的路径增广。
不能直接处理有负环的图。
正确性:归纳法?设图上没有负环,流量为 \(i\) 的最小费用为 \(f_i\) ,可得 \(f_0=0\)
在 \(f_i\) 得到的残量网络上找到最短路增广,计算得到 \(f_{i+1}\) ,则 \(f_{i+1}-f_{i}\) 是最短路长度
假设存在 \(f_{i+1}'<f_{i+1}\) ,显然是需要经过负环增广。
那既然有负环,我们向负环中增加流量,可不增加 \(s\) 流出流量让 \(f_i\) 更小,与假设矛盾
所以正确性有保证
复杂度上界是 \(O(nm|f|)\) ,基于最大流的算法改进实现。
改进 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
是什么也没有关系。
我们考虑跑完最短路后会发生什么:
- 所有点 \(u\) 满足 \(d_u\le d_v+w(v,u)\)
- 对于每一个 \(u\) 存在一点 \(v\) 使得 \(d_u=d_v+w(v,u)\)
而增广之后会破坏什么?
不会是 \(1\) ,只有可能是满流后导致 \(2\) 不满足,使得不能找到最短路上的增广路。
找增广后,当前流对应割的边集 \(\{(u,v)\mid u\in S, v\in T\}\) ,表示到 \(u\) 增广失败了。
找到 \(d=\min_v d_v+w(v,u)-d_u\)
所有访问过的点距离标号增加 \(d\) 。这样不会破坏性质 1,
而且至少有一条新的边进入了 \(d_u=d_v+w(v,u)\) 的子图
使用范围:不可直接处理有负权的边。
效率问题:在一些图上很快,在一些图上很慢。
-
优点:减少多次访问节点与队列维护。多路增广。
-
缺点:最差情况下,真的一次修改只能让 \(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
。
势:给每一个节点赋予一个新的标号 \(h_u\) ,叫做“势”只是因为与物理中的势有相似的性质。
在此基础上把边 \((u,v)\) 的长度修改为 \(w'(u,v)=w(u,v)+h_u-h_v\)
证明其可行性,需要三点。
-
在 \(w'\) 上跑最短路和在 \(w\) 上跑是等价的。
易得固定对于路径 \(p_1,p_2,\cdots,p_m\)
长度为 \(\sum_{i=2}^m w(p_{i-1},p_i)+(h_{p_{i-1}}-h_{p_i})\) ,拆开之后能消掉 \(h\) ,最后剩下 \(h_{p_1}-h_{p_m}\)
所以长度等于 \((h_{p_1}-h_{p_m})+\sum_{i=2}^m w(p_{i-1},p_i)\)
对于固定的起点、终点 \(p_1=S,p_m=T\) ,\((h_{p_1}-h_{p_m})\) 为定值,最短路也自然等价。
-
边权 \(w'\) 保持非负。势的初始化?
\(w'(u,v)=w(u,v)+h_u-h_v\ge 0\) ,整理, \(w(u,v)+h_u\ge h_v\)
即势能要满足三角不等式。所以先跑一遍
spfa
, \(h_u\) 初始化为最初的最短路即可。 -
如何修改势,正确性如何保证?
修改的结论,假设增广后源点 \(S\) 到 \(i\) 的距离是 \(d_i\) (修改边权后的距离)
只需给 \(h_i\) 加上 \(d_i\) 即可。
若能证明修改后边权保持非负,就是正确的。
-
原有的边。满足
\[\begin{aligned} d_u+w'(u,v) &\ge d_v\\ d_u+(w(u,v)+h_u-h_v) &\ge d_v\\ (d_u+h_u)+w(u,v)&\ge (d_v+h_v) \end{aligned} \]所以用 \(h_i+d_i\) 作为新的势能对原有的边满足条件。
-
在一轮增广后,由于一些 \((u,v)\) 边在增广路上。一定会满足 \(d_u+w'(u,v)=d_v\)
之后残量网络上会多出一些 \((v,u)\) 边,根据 \(w(u,v)=-w(v,u)\) 可以得到
\[\begin{aligned} d_u+w'(u,v) &= d_v\\ d_u+(w(u,v)+h_u-h_v) &= d_v\\ (d_u+h_u)&= (d_v+h_v)-w(u,v)\\ (d_v+h_v)+w(v,u)&=(d_u+h_u) \end{aligned} \]因此新增的边 \((v,u)\) 的边权非负。
-
-
得证,边权全部非负,可以用
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
的代码?
因为费用流的时间复杂度很玄学,没有固定哪一种跑得快。
比如流量很小,单路增广可能优于多路增广。
对复杂度的估计是需要练习的。
总结
理解好网络流的基本写法,才能有建模解题的底气。