网络流,huhangqi 都能看懂
定义
网络:一种特殊的有向图 (\(G=(V,E)\)),在一般的图中多了源点(s),汇点(t)和每一条边都有容量,指的是这条边能流过的最大的流量。
流量:\(f(x,y)\) 表示这条边已经流过了多少,\(f(x)\) 表示这个点往外流了多少,有 \(f(x)= \sum_{i} f(i,u) - \sum _i f(i,u)\)。这个流量是后期定义的,而不是原网络就有的。
s-t 割:\(\{S,T\}\) 为网络的一种划分,即 \(S\cup T = V,S\cap T = \varnothing\) 且 \(s\in S ,t\in T\),那么称 \(\{S,T\}\) 为原网络的 s-t 割(以下简称割),割的容量定义为 \(||S,T||= \sum_{u\in S}\sum_{v\in T} c(u,v)\) 。
网络流问题主要解决的是以下问题及其衍生问题。
-
最大流问题,找到合适的流,使得流最大的 \(f\)。
-
最小割问题,求得 \(\min||S,T||\) 。
-
最小费用最大流,给每一条边设定一个费用,只要经过这一条边就需要花费指定的费用,使得达到最大流的时候费用最小。
最大流
给一个定义剩余容量,容量与容量之差叫做剩余容量 \(c_f(u,v) = c(u,v) - f(u,v)\)。
然后对于所有剩余容量组成的网络定义为残量网络。
对于每一个 \(s\rightarrow t\) 的路径,我们成为增广路,而对于这一条路,我们填充流量的过程叫做增广。
在增广的过程中,我们需要建一个反向边,以便于我们退流。这条边的流量是 \(-f(u,v)\)。
有一个比较 naive 的写法,直接每一次 BFS 从 \(s\rightarrow t\) 的路径,直接增广一下,在反向边退流。直到流量不在增加位置。
这个算法即为 EK 算法,时间复杂度是 \(O(|V||E|^2)\)。一般来说也跑不满。
BFS 分层:给点表上到源点的距离。
多路增广:如果增广一次,最短路没有变化,可以继续增广,直到最短路变大为止。bfs 之后,去掉一些可能在最短路上的边。只要沿着这些边增广即可。
当前弧优化:就是每一次遍历到一个点都是从上一次遍历完的边继续遍历。这样子显然更快。
优化之后即为 dinic 算法。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=10005,inf=1e9;
int n,m,s,t,las[N],cnt;
struct Edge {
int v,nxt,now;
} e[N<<1];
void add(int u,int v,int w) {
e[++cnt] = {v,las[u],w};
las[u] = cnt;
e[++cnt] = {u,las[v],0};
las[v] = cnt;
}
namespace NF {
int dep[N],cur[N];
void init() {
memset(las,-1,sizeof las),cnt=1;
}
bool bfs() {
queue<int>q;
q.push(s);
memset(dep,0,sizeof dep);
dep[s]=1;
while(!q.empty()) {
int u=q.front();
q.pop();
for(int i = las[u],v=e[i].v ; ~i; i=e[i].nxt,v=e[i].v) {
if(!dep[v] && e[i].now )
dep[v] = dep[u] +1,q.push(v);
}
}
return dep[t];
}
int dfs(int u,int fl) {
if(u==t || !fl)return fl;
int res=0;
for(int &i=cur[u],v=e[i].v ,d; ~i; i=e[i].nxt,v=e[i].v) {
if(dep[v] == dep[u] +1 && (d = dfs(v,min(fl-res,e[i].now )))) {
e[i].now -= d;
e[i^1].now+=d;
res +=d;
if(res==fl)return res;
}
}
return res;
}
long long dinic() {
int maxflow=0;
while(bfs()) {
memcpy(cur, las, sizeof cur);
maxflow += dfs(s, inf);
}
return maxflow;
}
};
signed main() {
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
NF::init();
for(int i=1,u,v,w; i<=m; i++) {
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);
}
cout<<NF::dinic()<<endl;
return 0;
}
然后你发现这个时间复杂度是 \(O(|V|^2|E|)\)。看起来也没快多少。
但是在一般的随机图上,dinic 跑的特别快,在边权为 1 的图上甚至可以到 \(O(|E|\sqrt{ |V|}|)\)。
实测大概速度差 8 倍。
知周所众,网络流默认不卡 dinic,所以就没学预流推进 useless。
最小割
我们知道最大流等于最小割。
令 \(F\) 为最大流, \(C\) 为最小割。证明 \(F=C\)。
显然 \(F\ge C\) ,因为你对着最大流割一下,就能得到一个割。
对于任意一个割 \((S,T)\)。
有 \(F = f(s,t) = \sum_{u\in S,v\in T,(u,v)\in E} f(u,v)-\sum_{u\in T,v\in S,(u,v)\in E} f(u,v) \le \sum_{u\in S,v\in T,(u,v)\in E} f(u,v) \le \sum _{u\in S,v\in T,(u,v)\in E} c(u,v) = C\)
综上,\(F=C\)。
然后最小割直接通过最大流即可证明。
最小费用最大流
即费用流。
只需将 EK 算法或 Dinic 算法中找增广路的过程,替换为用最短路算法寻找单位费用最小的增广路即可。
称为 SSP 算法,不难,相信大家都会。

浙公网安备 33010602011771号