网络流
由于笔者写这篇博客时比现在还要菜得多,容易发现其中出现了较多的事实性错误,之后可能会找时间修一遍。
关于学习途径
显然有无数人在自学网络流的时候因为网上大部分题解的姿势都过于抽象而被劝退,所以提一下。可作参考。
首先不建议看我写的这个。
入门网络流的话建议看某 18 年日报,虽然没有具体的复杂度之类的证明,但是确实可以包你看懂,至少理解最大流算法什么的基本思想是够了。
然后讲解最严谨知识点最全面例题最多的一篇应该是 Alex_Wei 神仙的,但是阅读起来不太友好。
定义
大概包含我在内的不少人最开始就是被网络流的巨大丑定义劝退的。
比较优雅地说就是:
- 我们把城市抽象成一张有向图。
- 图上有很多很多的节点,其中有一个源点
和一个汇点 。(后面会出现无源汇) - 把连接每个点的水管抽象为有向边,它们都有一个容量
。 - 假设整座城市只有你一个无业游民,你在汇点等着喝水。
- 现在水厂在源点放水,水会顺着边的方向不停的流,一路流向你所在的汇点。
- 一条边的流量
就是这条边里流了多少水,而且这条边的流量不能超过容量,不然它就爆了。有 。 - 除了源汇点之外的每个点都不会漏水,所以流进一个点的流量和流出这个点的流量是相等的。
- 水厂不想浪费水,需要让流出来的所有水都进你嘴里,于是规定流出源点和流进汇点的流量相等,那么这个流进汇点的流量就叫整张网络的流量。
其它的一些定义和性质要用再提。
网络最大流
你想在汇点喝到尽量多的水,那你就需要通过规划每条边里流多少水来使这个网络的流量最大。现在我们需要一些算法来求出这个最大流量。
因为有着容量限制,这个问题并没有那么简单。
EK
- 增广路:从源点到汇点的一条使得当前流量可以增加的路径。
- 剩余流量:一条边的容量减去流量,即还能流多少水。
- 残量网络:剩余流量不为
的边组成的网络。
EK 就是直接通过 bfs 不停地找增广路,直到实在找不到了(即源汇点在残量网络上不连通)为止。bfs 时需要跳过那些已经被流满的边。
先不管它看上去对不对哈,考虑一下这玩意怎么实现。
显然每次找到的增广路的流量就是这条路径上的边的剩余流量的最小值,那么我们每次就把增广路上的边的剩余流量减去整条增广路的流量,不断重复这个过程,直至没有任何不经过剩余流量为
然而用脖子都能想到这么直接做有很大概率出来的不是最大流,所以我们要做一点处理,使得 EK 在跑的过程中可以不断修正流方案。
也就是建反边。我们每次确定一条增广路,就使路径上的所有边的反边容量加上目前这条增广路的流量,表示这条边上的流量有多少是可以被撤销的,显然初值是
这样当我们以后再走到这里时,从反边走过来,就可以看作是把之前从正边流过去的流量匀回来,就当作没流过这条边(或者是没流过那么多),同样进行容量加减处理。这样就达到了撤销效果。因为每次增广都会使流量增加,不存在一条边的流量被反复撤销导致死循环的情况。
然后关于怎么建反边:设正边编号为
于是跑就完啦。这个撤销机制相当有用,后面网络流的很多特性都是基于这个的。
然后因为使用了 bfs,且在增广过程中流量不断递增,于是时间复杂度理论上是
然后你就可以愉快地 AC 掉 P3376 和 P2740 了。
const ll I=1e18,N=207,W=1e4+7;
ll n,m,s,t,ans;
ll u[W],v[W],w[W],val[W];
ll vis[N],pth[N],lst[N];
vector<pll > e[N];
bool bfs() {
memset(vis,0,sizeof(vis));
queue<ll> q;
q.push(s),vis[s]=1;
while (!q.empty()) {
ll p=q.front();
q.pop();
if (p==t) return 1;
for (pll i:e[p]) if (!vis[i.first]&&val[i.second])
vis[i.first]=1,pth[i.first]=i.second,lst[i.first]=p,q.push(i.first);
}
return 0;
}
void EK() {
while (bfs()) {
ll res=I;
for (ll i=t;i!=s;i=lst[i]) res=min(res,val[pth[i]]);
for (ll i=t;i!=s;i=lst[i]) val[pth[i]]-=res,val[pth[i]^1]+=res;
ans+=res;
}
}
int main() {
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for (ll i=1;i<=m;i++)
scanf("%lld%lld%lld",&u[i],&v[i],&w[i]),val[i*2]=w[i],
e[u[i]].pb({v[i],i*2}),e[v[i]].pb({u[i],i*2+1});
EK();
cout<<ans;
return 0;
}
Dinic
但是感觉 EK 不够快啊,看看有没有可以优化的地方。
回顾 EK 的过程,发现它每次 bfs 居然只能找出一条增广路,那不在增广路上的信息是不是白白浪费了啊,有点可惜,考虑把它们利用起来,一次多求几条增广路。
首先我们跑一遍 bfs。设在当前残量网络上,源点到每个点的距离为
那我们是不是可以直接知道在当前的 bfs 条件下,有哪些点之间是可能有流量的,然后一次性榨干目前状态下的所有增广路呢?
所以知道了每个点的
这时还能流水的边可能会减少,于是再跑一遍 bfs 求出找完增广路的网络的深度信息,再 dfs……不断重复以上过程直到 dfs 找不出增广路为止。
于是我们学会了 Dinic。
- 但它比 EK 难写诶,而且这又 bfs 又 dfs 的,真的能比 EK 快吗?
还真不一定。
- 那它有啥用啊。
但是它能优化啊。
- 多路增广
发现一次 dfs 只能找出一条增广路好像跟 EK 没有什么差别,不如用一次 dfs 全部找出来,因为 Dinic 使你可以确定当前所有可以流的边。
也就是在 dfs 的过程中记目前这个点还有多少流量能用,回来的时候不用从头再找,继续用剩下的流量从这个点开始走去找即可。
这么搞完复杂度没有变化,是个常数优化。
- 当前弧优化
发现我们很可能大量地重复遍历了已经流满的路径。这个流满指的并不是一个点的出边被流满,而是从这个出边流出去会在后面被阻断,没法再流到汇点了。这个你无法快速判断,很不好。
发现由于 dfs 找增广路的特性,如果走完了一条边然后出来,之后如果再从这条边过去(注意正向反向边这里不看成同一条),是不会对流量产生任何贡献的。所以可以对每个点记录一下上次走了哪条边,下一次 dfs 到这直接接着记录的边再搜。
用链式前向星就记上次的边的编号,每次重新开始就把它当做 head 搞。
用 vector 就记上次的边的下标,下次从它开始。
注意这里不是跳过当前弧,因为当前弧上的边有可能还没流满,还能再流,是从当前弧开始继续搜。
(所以当前弧这个名字应该指的就是目前搜到的从源点开始的路径吧?)
在这个优化的加持下,Dinic 的效率直接起飞。理论大概是
其实有些东西就不需要在 bfs 的时候记了,没难写多少。个人认为它比 EK 优雅。
ll n,m,s,t;
ll u[W],v[W],w[W],val[W];
ll dis[N],cur[N];
vector<pll > e[N];
bool bfs() {
memset(dis,-1,sizeof(dis)),memset(cur,0,sizeof(cur));
queue<ll> q;
q.push(s),dis[s]=0;
while (!q.empty()) {
ll p=q.front();
q.pop();
if (p==t) return 1;
for (pll i:e[p]) if (dis[i.first]==-1&&val[i.second])
dis[i.first]=dis[p]+1,q.push(i.first);
}
return 0;
}
ll dfs(ll p,ll fl) {
if (p==t) return fl;
ll pf=0;
for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
cur[p]=ii;
if (val[i.second]&&dis[i.first]==dis[p]+1) {
ll nf=dfs(i.first,min(fl-pf,val[i.second]));
if (nf) {
pf+=nf,val[i.second]-=nf,val[i.second^1]+=nf;
if (pf==fl) break;
}
}
}
return pf;
}
ll din() {
ll res=0;
while (bfs()) res+=dfs(s,J);
return res;
}
void mian() {
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for (ll i=1;i<=m;i++)
scanf("%lld%lld%lld",&u[i],&v[i],&w[i]),val[i*2]=w[i],
e[u[i]].pb({v[i],i*2}),e[v[i]].pb({u[i],i*2+1});
cout<<din();
}
其它的玩意
好像还有更快的 ISAP 什么的东西,有机会再写。
封装
考虑到网络流这玩意一般就单纯用来建个模跑个最大流费用流啥的然后就扔了,单独弄一堆数组什么的去开一遍似乎不太优雅,所以个人认为封装一下用起来会舒服很多。
namespace D {
const ll N=2e5+7,M=6e5+7;
ll n,m,s,t,mxf;
ll f[M<<1];
ll cur[N],dis[N];
vector<pll> e[N];
void ini(ll _n) {
n=_n,m=1,mxf=0;
for (ll i=0;i<=n;i++) vector<pll>().swap(e[i]);
}
void add(ll x,ll y,ll z) { f[++m]=z,e[x].pb({y,m}),f[++m]=0,e[y].pb({x,m}); }
bool bfs() {
for (ll i=0;i<=n;i++) dis[i]=J,cur[i]=0;
queue<ll> q;
q.push(s),dis[s]=0;
while (!q.empty()) {
ll p=q.front();
q.pop();
if (p==t) return 1;
for (pll i:e[p]) if (f[i.se]&&dis[i.fi]>dis[p]+1)
dis[i.fi]=dis[p]+1,q.push(i.fi);
}
return 0;
}
ll dfs(ll p,ll fl) {
if (p==t) return fl;
ll nf=0;
for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
cur[p]=ii;
if (f[i.se]&&dis[i.fi]==dis[p]+1) {
ll cf=dfs(i.fi,min(fl-nf,f[i.se]));
nf+=cf,f[i.se]-=cf,f[i.se^1]+=cf;
if (fl==nf) return fl;
}
}
return nf;
}
void din(ll _s=0,ll _t=n) { s=_s,t=_t; while (bfs()) mxf+=dfs(s,J); }
}
基础应用
有个叫网络流 24 题的东西。应该包含了网络流的一些基础题目。
里面的题要用再讲。
P2756 飞行员配对方案问题
这玩意一眼二分图最大匹配,实际上也是可以用网络流做的。
如果源点向每个左部点,每个右部点向汇点连一条容量为
至于这种神秘的建图方法都是怎么想到的:或许是这种“每个人只能选一种配对方案”的限制让发明者联想到了网络流的容量限制吧……
用 Dinic 跑二分图匹配会比匈牙利快得多,是
P3254 圆桌问题
二分图多重匹配问题,即一个点可以连若干条边。把源点与左部点,右部点与汇点的容量调整一下,表示能连的边数就可以了。输出方案还是一样。
P2763 试题库问题
也差不多。源点向每个题连容量为
最小费用最大流
EK & Dinic
看我们用这么一堆管子居然获得了如此之大的流量,黑心水厂不开心了,于是规定:第
作为一位心系人民的 OIer,你肯定要坚持底线,不让流量变动丝毫。但是为了节省经费,你需要在流量不变的前提下找到最小费用。这就是最小费用最大流问题。
其实相当简单啊。我们只要保证在目前的状态下,所有水都流在从源点出发的关于单位水的费用的最短路上,等到有管道的容量被榨干再重新求一遍最短路,反复执行下去直到找不到增广路为止。这样就能保证费用最小。
所以只要把 bfs 换成 spfa 即可。
- Dijkstra 不行吗?
注意这里反向边的花费是正向边的相反数,相当于走回去就督促甲方退钱,所以 Dijkstra 跑不了。不过好像确实有一个叫 Primal-Dual 的东西可以做到 Dijkstra 求解最小费用最大流,但是一般也没哪个缺德的出题人会在网络流里卡 SPFA……
诶还真有人卡过。后面会写。
- 费用为负的话不会有负环吗?
如果原图没有负环,网络流在 spfa 过程中也就不会搜出负环,顶多搜出来一堆
环,不至于出问题。但是如果原图有负环的话就寄了,后面也会写。
对于 EK,增广路就是每次 spfa 搜到的源点到汇点的路径。
对于 Dinic,一条增广路合法仅当路径上所有相邻的点
于是你就可以愉快地过掉 P3381 了。
EK:
ll n,m,s,t,ans1,ans2;
ll u[W],v[W],w[W],c[W],val[W],cs[W];
ll vis[N],pth[N],lst[N],dis[N];
vector<pll > e[N];
bool spfa() {
memset(vis,0,sizeof(vis)),memset(dis,45,sizeof(dis));
queue<ll> q;
q.push(s),vis[s]=1,dis[s]=0;
while (!q.empty()) {
ll p=q.front();
q.pop();
vis[p]=0;
for (pll i:e[p]) if (dis[i.first]>dis[p]+cs[i.second]&&val[i.second]) {
dis[i.first]=dis[p]+cs[i.second];
pth[i.first]=i.second,lst[i.first]=p;
if (!vis[i.first]) vis[i.first]=1,q.push(i.first);
}
}
return dis[t]<I;
}
void EK() {
while (spfa()) {
ll res=I;
for (ll i=t;i!=s;i=lst[i]) res=min(res,val[pth[i]]);
for (ll i=t;i!=s;i=lst[i]) val[pth[i]]-=res,val[pth[i]^1]+=res;
ans1+=res,ans2+=res*dis[t];
}
}
int main() {
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for (ll i=1;i<=m;i++)
scanf("%lld%lld%lld%lld",&u[i],&v[i],&w[i],&c[i]),
val[i*2]=w[i],cs[i*2]=c[i],cs[i*2+1]=-c[i],
e[u[i]].pb({v[i],i*2}),e[v[i]].pb({u[i],i*2+1});
EK();
cout<<ans1<<" "<<ans2;
return 0;
}
Dinic:
namespace D {
const ll N=5e3+7,M=5e4+7;
ll n,m,s,t,mxf,mxc;
ll f[M<<1],c[M<<1];
ll cur[N],vis[N],dis[N];
vector<pll> e[N];
void ini(ll _n) {
n=_n,m=1,mxf=mxc=0;
for (ll i=0;i<=n;i++) vector<pll>().swap(e[i]);
}
void add(ll x,ll y,ll z,ll w) {
e[x].pb({y,++m}),f[m]=z,c[m]=w;
e[y].pb({x,++m}),f[m]=0,c[m]=-w;
}
bool spf() {
for (ll i=0;i<=n;i++) dis[i]=J,cur[i]=vis[i]=0;
queue<ll> q;
q.push(s),dis[s]=0;
while (!q.empty()) {
ll p=q.front();
q.pop(),vis[p]=0;
for (pll i:e[p]) {
if (f[i.se]&&dis[i.fi]>dis[p]+c[i.se]) {
dis[i.fi]=dis[p]+c[i.se];
if (!vis[i.fi]) vis[i.fi]=1,q.push(i.fi);
}
}
}
return dis[t]<J;
}
ll dfs(ll p,ll fl) {
if (p==t) return fl;
vis[p]=2;
ll nf=0;
for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
cur[p]=ii;
if (vis[i.fi]<2&&f[i.se]&&dis[i.fi]==dis[p]+c[i.se]) {
ll cf=dfs(i.fi,min(fl-nf,f[i.se]));
nf+=cf,mxc+=c[i.se]*cf,f[i.se]-=cf,f[i.se^1]+=cf;
if (fl==nf) return vis[p]=0,fl;
}
}
return vis[p]=0,nf;
}
void din(ll _s=0,ll _t=n) { s=_s,t=_t,mxc=mxf=0; while (spf()) mxf+=dfs(s,J); }
}
最大费用最大流
反过来变成最小费用。或者直接把 SPFA 换成最长路。如果有环也得做后面讲的负环处理。
Dijkstra 费用流
就是上面提到的 Primal-Dual。
前置知识:Johnson 全源最短路
就是通过一些神秘的技巧把负权边搞掉然后跑
具体地,建个超级源点,然后从它向所有点连一条边权为
简单解释一下:其实这扯到了一个叫势能法的东西。叫势能是因为势能只跟起点和终点有关,类比一下这个性质。你可以发现不管从起点到终点走的是哪条路径,
- 诶那我就可以用 Dijkstra 求负权图最短路了,那 SPFA 不就彻底死了吗。
你看看你预处理跑了个什么东西。
Primal-Dual
首先你正常地从源点出发求一遍所有点的
直接
挺显然的。这时你的
namespace din {
const ll N=5e3+7,M=1e5+7;
ll n,m,s,t,mxf,mxc;
ll dis[N],vis[N],h[N];
ll val[M],cs[M];
ll cur[N];
vector<pll > e[N];
void joh() {
memset(h,20,sizeof(h));
queue<ll> q;
q.push(s),h[s]=0,vis[s]=1;
while (!q.empty()) {
ll p=q.front(); q.pop(),vis[p]=0;
for (pll i:e[p]) if (val[i.se]&&h[i.fi]>h[p]+cs[i.se]) {
h[i.fi]=h[p]+cs[i.se];
if (!vis[i.fi]) q.push(i.fi),vis[i.fi]=1;
}
}
}
bool dij() {
memset(dis,20,sizeof(dis)),memset(vis,0,sizeof(vis)),memset(cur,0,sizeof(cur));
priority_queue<pll > q;
q.push({dis[s]=0,s});
while (!q.empty()) {
ll p=q.top().se; q.pop();
if (vis[p]) continue; vis[p]=1;
for (pll i:e[p]) if (val[i.se]&&dis[i.fi]>dis[p]+cs[i.se]+h[p]-h[i.fi])
q.push({-(dis[i.fi]=dis[p]+cs[i.se]+h[p]-h[i.fi]),i.fi});
}
return dis[t]<J;
}
ll dfs(ll p,ll fl) {
if (p==t) return fl;
vis[p]=2;
ll pf=0;
for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
cur[p]=ii;
if (vis[i.fi]!=2&&val[i.se]&&dis[i.fi]==dis[p]+cs[i.se]+h[p]-h[i.fi]) {
ll nf=dfs(i.fi,min(fl-pf,val[i.se]));
if (nf) {
pf+=nf,val[i.se]-=nf,val[i.se^1]+=nf;
if (pf==fl) break;
}
}
}
vis[p]=0;
return pf;
}
void mian() {
joh();
for (ll tmp=0;dij();) {
tmp=dfs(s,J);
for (ll i=1;i<=n;i++) h[i]+=dis[i];
mxf+=tmp,mxc+=tmp*h[t];
}
}
void ini(ll _n,ll _s,ll _t) { n=_n,s=_s,t=_t,m=1,mxf=mxc=0; }
void add(ll x,ll y,ll z,ll w) {
val[++m]=z,cs[m]=w,e[x].pb({y,m});
val[++m]=0,cs[m]=-w,e[y].pb({x,m});
}
}
有负环费用流
在后面上下界网络流那一块。
基础应用
通常费用流的用法是,用最大流满足题目限制,用最小费用满足答案最优。
P4014 分配问题
二分图最大匹配,但是带权。把之前最大流求二分图最大匹配中,左部点和右部点之间连的边带个费用即可。P4015 就是把容量改一下。
P4016 负载平衡问题
把货物搬运看成流,源点向每个点连容量为
……
最小割
水厂的黑心甲方们不高兴了,于是要阻止我们喝水。他们需要通过割断一些水管来使水无法从源点流到汇点。但是水厂的经费也有限,所以他们要使割掉的水管容量和最小。这个容量和叫最小割。
- 最大流最小割定理:最大流等于最小割。
口胡一下。
- 所有割都不小于最大流:
给一种感性理解。
每割掉一条边最多只会使整张网路的流量减少这条边的容量,即极限情况是这条边被流满了,而且没有其它的边还可流。显然你割这里不可能会使其它地方的流量变少。
那割到最后,割掉的边容量之和肯定不会还没最大流大。
- 存在一组割等于最大流:
反证,假设达到最大流的时候残量网络还连通,那么还可以继续增广,所以这时求出的不是最大流,矛盾。
回想一下,之前 EK 和 Dinic 判断是否达到最大流的条件就是源汇点是否在残量网络上连通,即还有没有增广路。
求方案
可以根据上面的证明感性理解,被割掉的边一定是被流满的。注意被流满的边不一定被割掉,如果有边权相同的就寄了。
所以从源点出发搜一遍,遇到流满的边就计入答案然后转头就跑,否则走过去继续搜。
基础应用
难绷的是最小割好像没什么简单应用题,就随便拿个凑个数吧。
Gym102341B Bulbasaur
给定一张
层每层 个点的分层图,每条边一定从 层连到 层。定义 为选择若干条从第 层到第 层的路径,并且每条路径上的点和边都不相交,最多选的路径数,求 。
, ,6s,512MB。
发现
好,网络流部分结束了。后面状压略过,还需要一个状态设计优化。
上下界网络流
见黑心水厂这么黑心,我们果断放弃,换了一个良心水厂。现在我们可以喝水了。
然而良心水厂也有一个要求:如果一条水管里几乎不流水,那么这会使得建筑方觉得它们辛辛苦苦打下的管子白费了,良心水厂觉得这不好,于是要求第
然后就可以引出一大堆问题。
无源汇上下界可行流
- ?我是谁?我在哪?水厂又在哪?
也就是说水厂在水管里放了一点违反了能量守恒定律的水就跑了,这些水在管子里不停的流,也就是说每个点都需要满足流进的水量等于流出的水量,即流守恒。我们需要规划一种流的方案使得它符合水厂的要求,咋办?
网络流算法是基于反悔的,也就是说只要是个合法网络,不管流没流过它都能跑。但前提是它必须是个合法的符合流量守恒的网络。
首先我们钦定每条边的下限都被流满了,然后将新的流量设为上限减去下限,再去考虑咋流。主要问题在于流满下限之后,每个点的流守恒很可能当场就没了,所以我们要做一些处理。
考虑把流不守恒的情况转化掉。虚空建一个超级源点和一个超级汇点,然后对每个点连边。如果一个点多了水就把漏的水量连到汇点上,少了水就让源点连过来负责加对应的水量,正好守恒就不管它。
这个时候再对这张每条边容量已经更换为上界减去下界,而且加上了超源超汇的图跑最大流。如果从源点出发的边存在没流满的,那说明不守恒的问题无法解决(即还没流满就增广不了了),不存在可行流;否则目前原图中的边流的方案即是一组可行流。
有源汇上下界可行流
水厂突然回来放水了。
相比无源汇的变化在于源点和汇点可以不满足流守恒,那就从原图中的汇点到源点连一条容量为
流量不是跑出来的最大流,应该是汇点到源点的无限边里的流量(或者反边的容量)。
有源汇上下界最大流
先跑一遍可行流,然后在跑完可行流后,撤掉超级源汇点和无限容量边,从源点到汇点再跑出一个最大流即可。答案是可行流量加上从源点到汇点的新最大流。
换句话说,加上超级源汇和无限边后用超源超汇跑一遍最大流,检查一下有没有流满然后把它们撤掉用原源原汇再跑一次最大流,两次加起来。
这时因为容量已经更换为上界减去下界,我们又用可行流提前规划好了一种可行的流法,那么最大流在上面怎么反悔都不会打破限制,还可以帮你完成一切的撤销工作,确保是最大流。
(Loj116)
namespace D {
const int N=207,M=2e4+7;
ll n,m,s,t,mxf;
ll f[M<<1];
ll cur[N],dis[N],flw[N];
vector<pll> e[N];
bool bfs() {
memset(dis,20,sizeof(dis)),memset(cur,0,sizeof(cur));
queue<ll> q;
q.push(s),dis[s]=0;
while (!q.empty()) {
ll p=q.front();
q.pop();
if (p==t) return 1;
for (pll i:e[p]) if (f[i.se]&&dis[i.fi]>dis[p]+1)
dis[i.fi]=dis[p]+1,q.push(i.fi);
}
return 0;
}
ll dfs(ll p,ll fl) {
if (p==t) return fl;
ll nf=0;
for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
cur[p]=ii;
if (f[i.se]&&dis[i.fi]==dis[p]+1) {
ll cf=dfs(i.fi,min(fl-nf,f[i.se]));
nf+=cf,f[i.se]-=cf,f[i.se^1]+=cf;
if (fl==nf) return fl;
}
}
return nf;
}
void din() { while (bfs()) mxf+=dfs(s,J); }
void ini(ll _n,ll _s,ll _t) {
n=_n,s=_s,t=_t,m=1,mxf=0,memset(flw,0,sizeof(flw));
for (ll i=0;i<=n+2;i++) e[i].clear();
}
void add(ll x,ll y,ll z) { f[++m]=z,e[x].pb({y,m}),f[++m]=0,e[y].pb({x,m}); }
void add(ll x,ll y,ll a,ll b) { add(x,y,b-a),flw[x]-=a,flw[y]+=a; }
void mian(ll _s,ll _t) {
s=++n,t=++n;
for (ll i=0;i<=n-2;i++) {
if (flw[i]>0) add(s,i,flw[i]);
else if (flw[i]<0) add(i,t,-flw[i]);
}
add(_t,_s,J),din();
for (pll i:e[s]) if (f[i.se]) return mxf=-1,void();
mxf=f[m],f[m-1]=f[m]=0,s=_s,t=_t,din();
}
}
ll n,m,s,t;
void mian() {
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
D::ini(n,s,t);
for (ll i=1,x,y,c,d;i<=m;i++) scanf("%lld%lld%lld%lld",&x,&y,&c,&d),D::add(x,y,c,d);
D::mian(s,t);
if (~D::mxf) cout<<D::mxf<<"\n";
else cout<<"please go home to sleep";
}
有源汇上下界最小流
- 最小流不是
吗?……哦。
最大流的从源点到汇点再跑一次最大流是看还有什么流量还可以加。
那我们是不是从汇点到源点跑一次看还有什么流量可以减就行了?
所以就是可行流量减去从汇点到源点的新最大流。
这里有一个定义问题:在某些神秘情况中,你可能会求出最小流是负的。其实根据网络流的形式化定义,源点的流量为流出流量减去流入流量。实际上流出流量小于流入流量也是可行的。但是题目限制可能会让这种情况不合法,所以需要针对各个题目进行修改。感觉这或许也是最小流的题好像没几个的原因……?
有源汇上下界费用流
一样地换成 spfa 啦。注意一开始找可行流要的费用也得加上。
基础应用
P7173 【模板】有负圈的费用流
你会发现你的 SPFA 或者 Dijkstra 跑着跑着就死了,所以我们要开点外挂。
考虑直接把网上的负边干掉!
还是一样,跑费用流之前先把每条负费用边流满是没有关系的:把它们的反边容量一样进行增加,费用是原边的相反数。然后图上就没有负边了,要改就等着后面流过来的时候反悔掉。
然后为了保住流量守恒,所以用回有源汇上下界网络流的技术,建超源超汇和无限边,少流量了用超源补,多流量了补给超汇,确保没有负权边后先跑一遍费用流。让网络暂时变得流量守恒,接下来把超源超汇撤掉,一切交给第二次费用流。
注意一开始处理满流的时候依然要计算费用。
P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流
建模是好想的。
- 照片是流。
- 源点向每个少女连容量为
的边。 - 每天可以拍的少女向这一天连容量为
的边。 - 每天向汇点连容量为
的边。
跑一遍上下界。
……
网络流的那些 trick
下面是一些网络流的常用模型。
值得注意的是,虽然这些模型用得多,但是大多数情况下是无法直接套用的,通常需要对这些模型有着深刻理解,并充分发挥人类智慧进行魔改才能做。
拆点大法
网络流针对的是边。如果有时建出的图对点里的流量/费用也有限制要怎么办呢?
把点拆成入点和出点,入点和出点中间连我们需要的流量/费用即可。
当然有时候限制的可能不是流量费用而是流的方式,也许可以多拆几个入点出点解决。
P4013 数字梯形问题
路径看成流,答案看成费用,尝试最大费用最大流解决。
把每个数字拆成入点和出点,连接出入点的边的容量和费用分别设置为每一问需要的数值即可。
P1251 餐巾计划问题
你发现瓶颈大部分在于怎么区分干净毛巾和脏毛巾。那就把每天拆成早上和晚上,流出早上的是干净毛巾,流进晚上的是脏毛巾。那这些干净毛巾给谁,脏毛巾从哪来呢?你发现还有源汇点没有用,于是接上去就好了。
具体的,以下是离谱的建模方法:
- 流即为毛巾。
- 源点提供各种毛巾,汇点收集干净毛巾然后变成脏毛巾丢给源点,由于流量守恒,这样不会有毛巾被弄丢,可以理解成是毛巾通过汇点流回了源点。
- 每天拆成早上和晚上两个点,以区分干净和脏毛巾。
- 规定每条毛巾在洗好之后立刻开始用,如果要晚点用就等完再洗。
- 提供所需干净毛巾:每天早上向汇点连边,容量为当天
,费用为 。 - 生产脏毛巾:源点向每天晚上连边,容量为当天
,费用为 。 - 留着脏毛巾之后再处理:每天晚上向下一个晚上连边,容量为
,费用为 。 - 买毛巾:源点向每一天早上连边,容量为
,费用为 。 - 慢洗部:每天晚上向
天后的早上连边,容量为 ,费用为 。 - 快洗部:每天晚上向
天后的早上连边,容量为 ,费用为 。 - 由于只有前两类边有容量限制,所以跑最大流肯定能把这两类边全部流满,也就是保证每天的毛巾都够用。在此基础上再去保证费用最小。
- 也就是在这个网络上跑费用流。
最小费用满足费用最小,最大流满足每天毛巾够用了。这个建模手法感觉还挺人类智慧的,膜拜发明者一秒。利用流量守恒表示物品个数不变应该算个套路罢。
最小路径覆盖
一张 DAG,请你用若干条没有公共点的简单路径覆盖上面的所有点。求最小路径数。
你或许可以每个点跟源点和汇点连容量为
根据点做看起来没什么前途了,考虑根据边做。你发现路径数最少其实就等价于没被覆盖的边数最少,因为每条路径覆盖的边数就是点数
你想起了二分图匹配。每条边
很明显在一般图上这么做的话它会把环算进去,那么边数最少就不等价于路径最少了,因为会多出一条,可能另有他用。
P2764 最小路径覆盖问题
模板。输出方案跟二分图一样。
P2765 魔术球问题
你大胆猜测答案不会太大。那么对于每一对和为完全平方数的
然后你发现
P8291 [省选联考 2022] 学术社区
重量级选手来了。
考虑处理出每两条信息接在一起时会带来多少贡献。发现贡献可能有
那么现在问题就变为我们要在这个
考虑没有
你发现没有太大的问题,因为题目有一个反复强调的性质就是每个人都发过至少一条学术消息!因为环肯定全是 louxia 或全是 loushang,我们可以随便挑环上的一个位置劈开,然后把断开位置的那个人发的学术消息接到前面/后面,相当于贡献并没有减少,而整个断开的环加上学术消息就相当于又变成了同一个人发的超大学术消息,可以继续用。
然后把 a b loushang
和 b a louxia
,可以等效为一条从前面看是
但是边数达到了惊人的
输出方案的话就各显神通吧。
集合划分
有
个元素,你要将它们分到两个集合 中,已知第 个元素分配到 集合的代价是 , 集合的代价是 ,又有 组关系 , 不在同一个集合中的代价是 。问最小划分代价。
考虑最小割。
- 源点向每个元素连容量为
的边。 - 每个元素向汇点连容量为
的边。 - 每对
间连容量为 的双向边。 - 跑一遍最小割(最大流)即为答案。其中每个元素如果连源点就代表被分到
集合,连汇点就代表被分到 集合。注意前面容量是反的。
不难理解。其中每对关系可以实现是因为如果
几个变形:
中有非正数:把 在图上对应的容量同时加上一个大常数,使得你每个元素不管分到哪个集合都得选这个大常数的代价,最后去掉 倍的这个常数即可。- 关系不只是
在不同集合,而是确定 在 集合, 在 集合之类:把双向边改成单向边。 - 关系限制变为
在同一个集合中的代价:这个不属于集合划分,看后面最大独立集。
P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查
模板。
Gym100729F Pool construction
草地和洞是两个集合。处理出每一块变草地/变洞的代价,然后相邻的块存在上面说的关系。注意外围必须是草地,所以如果原本是洞的话保持洞的代价是
最大独立集
这个应该归到二分图里的。
一张二分图,每个点带权,求它的最大独立集。
- 源点向每个左部点,每个右部点向汇点连容量为权值的边。
- 原图中的边容量为
。 - 跑一遍。
由于有结论最大独立集=点数-最大匹配,这个并不难理解。
其实从最小割的角度看的话这个结论也可以得到证明。如果一条边连接的两个点没有被割掉,那么我们就需要割掉这条边,而它的容量是巨大的,这不好。
拓展:
一张二分图,每个点带权,每条边也带权,你要选一些点,同时也会选两个端点都被选的边,求最大权值。
把上面的原图边容量改成边权就行了。结合最小割也不难理解。
- 为什么不是二分图就没法做?
一般图的最大独立集问题已被证明是 NP 问题。而且你似乎也想不到不是二分图的时候该怎么建图啊。
P2774 方格取数问题
你发现这是一个二分图,考虑最大独立集。
- 将网格黑白染色。
- 把黑点和白点分别拎出来排成两排。
- 源点向黑点,白点向汇点连容量为对应
的边。 - 网格上相邻的黑白点之间连容量为
的边。 - 跑最小割。
减去结果即为答案。
P5030 长脖子鹿放置
也许你会在做完方格取数问题之后满怀自信地点开这题,然后……不会了。
感觉网络流题确实非常需要避免思维固化,实际上这题把上一个题的黑白染色换成按横坐标奇偶性染色就可以了。
Gym102428A Swap Free
根据交换关系建出图,发现是一个最大独立集问题,如果是一般图就寄了,所以肯定要找点性质才能做。
你发现这个图上一定没有奇环,于是就可以做了。
最大闭合子图
- 给定一张有向图,每个点带权。请你求一个子图,使得图上所有点的出边都在子图中,并使子图内的点的点权之和最大。
考虑最小割。
- 源点向所有非负权点连容量为对应权值的边,所有负权点向汇点连容量为对应权值的相反数的边。
- 保留原图的所有边,容量为
。 - 跑一遍最小割(最大流),所有正权的和减去最小割结果即为答案。没被割掉的边连接的点就在子图中。
这里原图的边实在太大,所以割的肯定全是附加边。而对于源点连接的没被割掉的边,可以发现从它们连的点往外流,流到不能流的时候就是一个闭合子图,这时再把负权点连的边割掉就可以不连通了。可以发现这个最小割和最后的减去答案很巧妙地处理了正权/负权边以及答案最优。
P4174 [NOI2006] 最大获利
模板。
P2762 太空飞行计划问题
还是模板。
最大密度子图
- 求无向图上的一个导出子图,使得子图中边与点的比值最大。
设原图为
比值问题先二分答案试试。设答案为
简单但复杂度没那么优做法
考虑子图有什么限制。也就是要选一条边,那么就必须要选它连接的两个点。如果把每条边也看成点的话,我们惊喜地发现这就是一个最大闭合子图问题。
- 每条原图中的边代表的点向它连接的两个点连一条边。
- 每条原图的边代表的点有
的点权,每个原图的点有 的点权。 - 直接跑最大闭合子图。
非常好理解,但是点数是
复杂度优但没那么简单做法
考虑能不能把每条边的选取情况用点的信息描述。你惊喜地发现有一个东西叫做度数!
设第
不完全是。集合划分求的是最小值,这玩意要最大值
考虑魔改集合划分。
- 源点向每个点连容量为
的边。 - 每个点向汇点连容量为
的边。 - 每对原图中的无向边的容量改为
。(这个是 )
UVA1389 Hard Life
模板。输出方案就是输出集合划分的方案也就是输出最小割的方案。
(to be continued)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】