网络流学习笔记
1 - 最大流
下面那个什么能跑 \(1e4,1e5\) 的是前人经验,没拿数据测试过,不过一般网络流题的复杂度好像是玄学,数据范围算出来不是太离谱都能做……
问题概述
有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点)。
Edmond-Karp(EK)
复杂度:\(\mathcal{O}(nm^2)\) ,处理范围在 \(1e3\sim 1e4\) 左右。
思路:从源点到汇点有很多条路径,每次抽取一条,并找出路径上的边权最小值 \(x\) ,然后把路径上所有边权都减去\(x\)(也就是找增广路,这条路径上流过 \(x\) 的流量),直到没有路径可以给汇点增加流量为止。显然, \(0\) 容量是没有用的。具体实现采用 BFS 。
如果是单纯的建边跑不了最大流,因为如果之前选的并不是最优路径后面就不一定能取到最大值。EK 支持一个 反悔操作 ,也就是建反向边,初始为 \(0\) ,如果对应的正向边剩余容量减少,那么相应的反向边容量就增加,之后反向边就能流动了。
//Author:RingweEH
//P3376 【模板】网络最大流
const int N=210,M=5e3+10;
const ll INF=0x7f7f7f7f;
struct Edge
{
int to,nxt; ll val;
}e[M<<1];
int n,m,S,T,tot=1,head[N],pre[N],mp[N][N];
ll mxflow,dis[N];
bool vis[N];
void Add( int u,int v,int ll w )
{
e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; e[tot].val=0; //反向边
}
bool BFS()
{
for ( int i=1; i<=n; i++ )
vis[i]=0;
queue<int> q; q.push( S ); vis[S]=1; dis[S]=INF;
while ( !q.empty() )
{
int u=q.front(); q.pop();
for ( int i=head[u]; i; i=e[i].nxt )
{
if ( e[i].val==0 ) continue; //流没了
int v=e[i].to;
if ( vis[v] ) continue;
dis[v]=min( dis[u],e[i].val ); //取路径上的最小剩余容量
pre[v]=i; //记录从哪条边过来
q.push( v ); vis[v]=1;
if ( v==T ) return 1; //走到汇点了
}
}
return 0;
}
void Update() //每次找到一条增广路,并更新路径上的值
{
int u=T;
while ( u^S )
{
int v=pre[u];
e[v].val-=dis[T]; e[v^1].val+=dis[T];
//这条增广路的流量,用来更新路径上所有边和反向边
u=e[v^1].to; //本来记录的是过来的边,回去要反向
}
mxflow+=dis[T]; //总流量增加
}
int main()
{
n=read(); m=read(); S=read(); T=read();
for ( int i=1; i<=m; i++ )
{
int u,v; ll w; u=read(); v=read(); w=read();
if ( mp[u][v]==0 ) Add( u,v,w ),mp[u][v]=tot;
else e[mp[u][v]-1].val+=w;
//重边容量合并
}
while ( BFS() ) Update();
printf( "%lld\n",mxflow );
return 0;
}
Dinic
复杂度 \(\mathcal{O}(n^2m)\) ,处理范围在 \(1e4\sim 1e5\) 左右,稠密图较明显,求解二分图最大匹配是 \(\mathcal{O}(m\sqrt n)\) .
思路:BFS 得到分层图,\(d[x]\) 表示层次(即 \(S\) 到 \(x\) 的最少边数)。残量网络中,满足 \(d[y]=d[x]+1\) 的边 \((x,y)\) 构成的子图称为分层图。显然,这一定是一张有向无环图,从 \(S\) 开始 DFS ,每次向下找一个点,直到到达 \(T\) ,然后回溯回去,找另外的点搜索出多条增广路。
总结:
- 在残量网络上 BFS 求出节点层次,构造分层图
- 在分层图上 DFS 找增广路,回溯的同时更新边权
当前弧优化 :(没加这个复杂度是假的)
对于一个节点 \(u\) ,在 DFS 中, for ( int i=head[u]; i; i=e[i].nxt )
走到了第 \(i\) 条弧时,前 \(i-1\) 条到汇点 \(T\) 的路径一定流满了,再访问 \(u\) 时前 \(i-1\) 条就没有意义了。所以每次枚举 \(u\) 的时候,要改变枚举的起点,即 \(nw\) ,for ( int i=now[x]; i; i=e[i].nxt )
.
//Author:RingweEH
//P3376 【模板】网络最大流
const int N=210,M=5e3+10;
const ll INF=0x7f7f7f7f;
struct Edge
{
int to,nxt; ll val;
}e[M<<1];
int n,m,S,T,tot=1,nw[N],head[N];
ll mxflow,dis[N];
void Add( int u,int v,ll w )
{
e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; e[tot].val=0;
}
bool BFS() //构建分层图,判断残量网络还能不能到达汇点
{
for ( int i=1; i<=n; i++ )
dis[i]=INF;
queue<int> q; q.push( S ); dis[S]=0; nw[S]=head[S];
while ( !q.empty() )
{
int u=q.front(); q.pop();
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( e[i].val>0 && dis[v]==INF )
{
q.push( v ); nw[v]=head[v]; dis[v]=dis[u]+1;
if ( v==T ) return 1;
}
}
}
return 0;
}
int DFS( int u,ll sum ) //sum记录增广路上的最小容量
{
if ( u==T ) return sum;
ll res=0;
for ( int i=nw[u]; i && sum; i=e[i].nxt )
{
nw[u]=i; int v=e[i].to;
if ( e[i].val>0 && (dis[v]==dis[u]+1) )
{
ll tmp=DFS( v,min(sum,e[i].val) );
if ( tmp==0 ) dis[v]=INF; //v不能再增广了,剪枝
e[i].val-=tmp; e[i^1].val+=tmp;
res+=tmp; sum-=tmp; //res是经过该点的流量和,sum是经过该点剩余流量
}
}
return res;
}
int main()
{
n=read(); m=read(); S=read(); T=read();
for ( int i=1; i<=m; i++ )
{
int u=read(),v=read(); ll w=read();
Add( u,v,w );
}
while ( BFS() ) mxflow+=DFS(S,INF);
printf( "%lld\n",mxflow );
return 0;
}
ISAP
复杂度:\(\mathcal{O}(n^2m)\)
看看 Dinic 的 while ( BFS() ) mxflow+=DFS(S,INF);
,这样重复 BFS 显然效率低下。于是出现了 ISAP ,只需要跑一遍 \(bfs\) !
大体流程:
- 从 \(T\) 到 \(S\) 跑 BFS
- 从 \(S\) 到 \(T\) 跑 DFS
- 重复第二步的 DFS 直到出现断层
ISAP 只跑一遍 BFS 标记层次,然后每个点的层次随着 DFS 变高。记 \(gap[i]\) 表示高度为 \(i\) 的点的个数,\(d[i]\) 表示 \(i\) 的层次,当结束 \(i\) 的增广之后,遍历残量网络上 \(i\) 的所有出边,找到 \(d\) 最小的出点 \(j\) ,令 \(d[i]=d[j]+1\) ,如果没有出边,\(d[i]=n\) 。当 \(gap[i]=0\) 时出现断层,\(S\) 和 \(T\) 不连通,就可以停止了(这其实是 GAP 优化)。
和 Dinic 类似,ISAP 同样有当前弧优化。且最短路的修改具有连续性,每次不需要真的遍历残量网络出边,直接给 \(d[i]\) 加一即可。
//Author:RingweEH
const int N=210,M=5e3+10;
const ll INF=0x7f7f7f7f;
struct Edge
{
int to,nxt; ll val;
}e[M<<1];
int n,m,S,T,tot=1,head[N],dep[N],gap[N],nw[N];
ll mxflow=0;
void Add( int u,int v,ll w )
{
e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; e[tot].val=0;
}
void BFS()
{
memset( dep,-1,sizeof(dep) ); memset( gap,0,sizeof(gap) );
dep[T]=0; gap[0]=1;
queue<int> q; q.push( T );
while ( !q.empty() )
{
int u=q.front(); q.pop();
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( dep[v]>-1 ) continue;
q.push( v ); dep[v]=dep[u]+1; gap[dep[v]]++;
}
}
}
int DFS( int u,ll sum )
{
if ( u==T ) { mxflow+=sum; return sum; }
ll res=0;
for ( int i=nw[u]; i; i=e[i].nxt )
{
nw[u]=i; int v=e[i].to;
if ( e[i].val && dep[v]+1==dep[u] )
{
int tmp=DFS( v,min(e[i].val,sum) );
if ( tmp )
{
e[i].val-=tmp; e[i^1].val+=tmp;
sum-=tmp; res+=tmp;
}
if ( sum==0 ) return res;
}
}
gap[dep[u]]--;
if ( gap[dep[u]]==0 ) dep[S]=n+1;
dep[u]++; gap[dep[u]]++;
return res;
}
int main()
{
n=read(); m=read(); S=read(); T=read();
for ( int i=1; i<=m; i++ )
{
int u=read(),v=read(); ll w=read();
Add( u,v,w );
}
BFS(); mxflow=0;
while ( dep[S]<n )
{
memcpy( nw,head,sizeof(head) ); DFS( S,INF );
}
printf( "%lld\n",mxflow );
return 0;
}
HLPP(最高标号预流推进)
前面的三种算法都是基于 增广路 的思想。
而这里是另一类:预流推进 。
预流推进思想:允许水流在非源汇的节点中暂时保存(称为 超额流 ),同时伺机将超额流 推送 出去。只要最后所有的节点超额流为 \(0\) ,那么就是合法的。为了避免死循环,引入每个节点的 高度 ,只允许高度较高的节点向高度较低的推送。如果一个节点因为高度原因不能推送出去,那么就抬高这个高度,这个操作称为 重贴标签 。
HLPP算法流程:用 \(e[v]\) 表示超额流,\(h[v]\) 表示高度。
首先,将所有除了源点以外的节点高度设为 \(0\) ,源点 \(S\) 设为 \(n\) ,并将 \(S\) 的所有边都充满流量推送出去(放水)。将所有推送后超额流不为 \(0\) 的节点加入以高度为关键字的优先队列中等待推送。
然后,每次取出对首 \(u\) 并尝试推送:
- 逐一检查 \(u\) 的出边,如果某条边还有流量,并且另一端 \(v\) 满足 \(h[v]+1=h[u]\) ,那么这条边可以推送。
- 推送流量不能超过边的容量,也不能超过 \(e[u]\)
- 推送完毕后修改超额流,如果 \(v\) 不在队列中那么加入
完成之后,如果 \(e[u]>0\) ,那么说明需要对 \(u\) 重贴标签。找到有流量并且另一端 \(h[v]\) 最小的边,令 \(h[u]=h[v]+1\) ,把操作完的 \(u\) 加入优先队列。
直到队列为空。
虽然这样复杂度正确,为 \(\mathcal{O}(n^2\sqrt m)\) ,但是需要一些优化才能在随机数据下和增广路算法相当。
- 可以通过一边 BFS 将每个点的初始高度设为到汇点的距离。(除了 \(S\) )
- 如果一个点被重贴标签之后,原来的高度已经没有点了,那么高于它的点必然不能推到 \(T\) ,所以可以将高度大于 \(h[u]\) 并且小于 \(n+1\) 的 \(h\) 设为 \(n+1\) ,以便尽快推给 \(S\) 。也就是和 ISAP 一样的 GAP 优化。
注意 gap 大小要开两倍。
然而事实上博主的写法并不适合我这种大常数选手。于是跑到 LOJ 上去学习了一番最优解,得到了如下写法。
这写法长这样:
记录高度为 \(i\) 的节点个数,具体节点,高度为 \(i\) 的有超额流的节点,记录更改高度的总次数,记录最大高度;
每次重贴标签直接从 \(T\) 开始重新对着残量网络 BFS;
一边往外流一边记录所有出边的端点高度最小值(为了有剩余超额流的时候直接更新高度);
断层就直接把所有高度比当前高的节点置为 INF ;
初始化源点无限流量,先重贴一遍,然后按照高度从高到低依次往外推流,如果更改高度的总次数大于某个阀值,那么就再重贴一遍。
具体可以看代码QWQ 跑得飞快。
//Author:RingweEH
#define pb push_back
const int N=1203,INF=0x3f3f3f3f;
struct Edge
{
int to,nxt,val;
Edge ( int _to,int _nxt,int _val ) : to(_to),nxt(_nxt),val(_val) {}
};
vector<Edge> g[N];
int n,m,S,T,h[N],cnt[N],work,ht,rest[N];
vector<int> las[N],gap[N];
void Add( int u,int v,int w )
{
g[u].pb( Edge(v,g[v].size(),w) );
g[v].pb( Edge(u,g[u].size()-1,0) );
}
void Update_height( int u,int nw )
{
++work; //work记录更改高度的次数
if ( h[u]^INF ) --cnt[h[u]];
h[u]=nw;
if ( nw==INF ) return;
++cnt[nw]; ht=nw; //cnt[i]记录高度为i的节点个数
gap[nw].pb(u); //gap[i]记录高度为i的节点
if ( rest[u] ) las[nw].pb(u); //las[i]记录高度为i且还有超额流的节点
}
void Relabel() //重置高度
{
work=0; memset( h,0x3f,sizeof(h) ); memset( cnt,0,sizeof(cnt) );
for ( int i=0; i<=ht; i++ )
las[i].clear(),gap[i].clear();
h[T]=0; queue<int> q; q.push(T);
while ( !q.empty() ) //每次重新BFS
{
int u=q.front(); q.pop();
for ( Edge &e : g[u] )
if ( h[e.to]==INF && g[e.to][e.nxt].val ) //如果还有流量且另一个端点没被更新过
{
q.push( e.to ); Update_height( e.to,h[u]+1 ); //设为到汇点的距离
}
ht=h[u]; //记录最大的一个高度
}
}
void Push( int u,Edge &e ) //u的超额流通过边e往外流
{
if ( rest[e.to]==0 ) las[h[e.to]].pb(e.to); //如果没在里面,现在就要在了
int del=min( rest[u],e.val );
e.val-=del; g[e.to][e.nxt].val+=del;
rest[u]-=del; rest[e.to]+=del;
}
void Push_Flow( int u )
{
int nh=INF;
for ( Edge &e : g[u] )
if ( e.val )
if ( h[u]==h[e.to]+1 )
{
Push( u,e );
if ( rest[u]<=0 ) return;
}
else nh=min( nh,h[e.to]+1 );
//一边往外流一边以防万一要更新,收集最小值
if ( cnt[h[u]]>1 ) Update_height( u,nh ); //没断层,直接更新
else
for ( int i=h[u]; i<N; i++ ) //断层了
{
for ( int j : gap[i] )
Update_height( j,INF );
gap[i].clear();
}
}
int HLPP()
{
memset( rest,0,sizeof(rest) );
rest[S]=2147483647;
Relabel();
for ( Edge &e : g[S] ) //从源点往外推
Push( S,e );
for ( ; ~ht; --ht )
while ( !las[ht].empty() ) //枚举高度,从高到低依次往外推
{
int u=las[ht].back(); las[ht].pop_back();
Push_Flow( u );
if ( work>20000 ) Relabel(); //如果改变得过多了那么就重新标记
}
return rest[T];
}
int main()
{
n=read(); m=read(); S=read(); T=read();
for ( int i=1,u,v,w; i<=m; i++ )
u=read(),v=read(),w=read(),Add( u,v,w );
printf( "%d\n",HLPP() );
}
2 - 最小割
概念&定理
对于一个网络流图 \(G=(V,E)\) ,将所有点划分成 \(S\) 和 \(T=V-S\) ,其中源点 \(s\in S\) ,汇点 \(t\in T\) . 定义割 \((S,T)\) 的容量为所有 \(S\) 到 \(T\) 的边的容量之和。
最大流最小割定理 :最大流=最小割,即 \(f(s,t)_{max}=c(S,T)_min\) 。证明
求方案:可以从源点 \(s\) 开始 DFS ,每次走残量大于 \(0\) 的边,找到所有 \(S\) 点集内的点。
求割边数量:将容量变为 \(1\) ,跑 Dinic 即可。
模型
有两个集合 \(A,B\) 和 \(n\) 个物品,第 \(i\) 个物品放入 \(A\) 产生花费 \(a_i\) ,放入 \(B\) 为 \(b_i\) ,还有若干个限制 \((u_i,v_i,w_i)\) ,如果 \(u_i\) 和 \(v_i\) 不在同一个集合里花费 \(w_i\) ,每个物品只能属于一个集合,求最小代价。
二者选其一 的最小割。对每个集合设置源点 \(s\) 和汇点 \(t\) ,第 \(i\) 个点由 \(s\) 连一条容量为 \(a_i\) 的边,向 \(t\) 连一条容量为 \(b_i\) 的边,在 \(u_i,v_i\) 之间连容量为 \(w_i\) 的双向边。当源汇不相连的时候,代表这些点都选择了其中一个集合。如果将连向 \(s\) 或者 \(t\) 的边割开,表示不放在 \(A\) 或者 \(B\) ,如果物品间割开,表示不放在同一个集合。最小割就是最小花费。
3 - 费用流
每条边除了容量限制还有单位流量的费用 \(w(u,v)\) 。当 \((u,v)\) 的流量为 \(f(u,v)\) 时,花费 \(f(u,v)\times w(u,v)\) .
研究问题:最小费用最大流
SSP
SSP(Successive Shortest Path)算法:在最大流的 EK 算法求解最大流的基础上,把 用 BFS 求解任意增广路 改为 用 SPFA 求解单位费用之和最小的增广路 即可。
相当于 把费用作为边权,在残存网络上求最短路 。
//Author:RingweEH
const int N=410,M=15010,INF=0x3f3f3f3f;
struct Edge
{
int to,nxt,flow,cap;
Edge ( int _to=0,int _nxt=0,int _flow=0,int _cap=0 ) :
to(_to),nxt(_nxt),flow(_flow),cap(_cap) {}
}e[M<<1];
int n,m,S,T,tot=1,head[N];
int pre[N],dis[N],mxflow,mncost;
bool vis[N];
void Add( int u,int v,int fl,int cap )
{
e[++tot]=Edge(v,head[u],fl,cap); head[u]=tot;
e[++tot]=Edge(u,head[v],0,-cap); head[v]=tot;
}
bool BFS()
{
for ( int i=1; i<=n; i++ )
vis[i]=0,dis[i]=INF;
queue<int> q; q.push( S ); dis[S]=0;
while ( !q.empty() )
{
int u=q.front(); q.pop(); vis[u]=0;
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( dis[v]>dis[u]+e[i].cap && e[i].flow )
{
dis[v]=dis[u]+e[i].cap; pre[v]=i;
if ( !vis[v] ) vis[v]=1,q.push( v );
}
}
}
return dis[T]!=INF;
}
void Update()
{
int mn=INF;
for ( int i=T; i^S; i=e[pre[i]^1].to )
bmin( mn,e[pre[i]].flow );
for ( int i=T; i^S; i=e[pre[i]^1].to )
{
e[pre[i]].flow-=mn; e[pre[i]^1].flow+=mn;
mncost+=mn*e[pre[i]].cap;
}
mxflow+=mn;
}
int main()
{
n=read(); m=read(); S=1; T=n;
for ( int i=1,u,v,f,c; i<=m; i++ )
u=read(),v=read(),f=read(),c=read(),Add( u,v,f,c );
while ( BFS() ) Update();
printf( "%d %d\n",mxflow,mncost );
}
类 Dinic
在 Dinic 算法的基础上进行改进,把 BFS 求分层图 改为用 SPFA求一条单位费用之和最小的路径(由于有负权的反向边,不能直接用 Dijkstra ),相当于 把 \(w(u,v)\) 当做边权在残量网络上求最短路 。
优化:使用 Primal-Dual 原始对偶算法,将 SPFA 改成 Dijkstra (有需要看 这篇 ,不过 代码也不长? )
复杂度上界:\(\mathcal{O}(nmf)\)
//Author:RingweEH
const int N=410,M=15010,INF=0x3f3f3f3f;
struct Edge
{
int to,nxt,flow,cap;
Edge ( int _to=0,int _nxt=0,int _flow=0,int _cap=0 ) :
to(_to),nxt(_nxt),flow(_flow),cap(_cap) {}
}e[M<<1];
int n,m,S,T,tot=1,head[N],dis[N],mxflow,mncost,nw[N];
bool vis[N];
void Add( int u,int v,int fl,int cap )
{
e[++tot]=Edge(v,head[u],fl,cap); head[u]=tot;
e[++tot]=Edge(u,head[v],0,-cap); head[v]=tot;
}
bool BFS()
{
for ( int i=1; i<=n; i++ )
dis[i]=INF,vis[i]=0;
queue<int> q; q.push( S ); dis[S]=0; nw[S]=head[S]; vis[S]=1;
while ( !q.empty() )
{
int u=q.front(); q.pop(); vis[u]=0;
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( e[i].flow && dis[v]>dis[u]+e[i].cap )
{
nw[v]=head[v]; dis[v]=dis[u]+e[i].cap;
if ( !vis[v] ) vis[v]=1,q.push(v);
}
}
}
return dis[T]!=INF;
}
int DFS( int u,int sum )
{
if ( u==T ) return sum;
vis[u]=1; int res=0;
for ( int i=nw[u]; i && sum; i=e[i].nxt )
{
nw[u]=i; int v=e[i].to;
if ( !vis[v] && e[i].flow && (dis[v]==dis[u]+e[i].cap) )
{
int tmp=DFS( v,min(sum,e[i].flow) );
if ( tmp )
{
e[i].flow-=tmp; e[i^1].flow+=tmp;
res+=tmp; sum-=tmp; mncost+=e[i].cap*tmp;
}
}
}
vis[u]=0; return res;
}
int main()
{
n=read(); m=read(); S=1; T=n;
for ( int i=1,u,v,f,c; i<=m; i++ )
u=read(),v=read(),f=read(),c=read(),Add(u,v,f,c);
while ( BFS() ) mxflow+=DFS(S,INF);
printf( "%d %d\n",mxflow,mncost );
return 0;
}
4 - 上下界网络流
解决每条边流量有上下界限制的网络流。
无源汇上下界可行流
问题 :有 \(n\) 个点, \(m\) 条边,每条边有一个流量下界 \(low\) 和流量上界 \(up\) ,求可行方案使得所有点满足流量平衡的前提下,所有边满足流量限制。
思路 :两个限制同时考虑会很麻烦,先考虑下界。
先给每条弧加一个 \(low\) 的流量下界,那么这个限制就没了。但是现在这个图不一定满足流量平衡,称为初始流。
应该在这张图上再添加一个附加流,使得初始流和附加流之和满足流量平衡。显然,每条弧的附加流上界是 \(up-low\) ,所以不妨在建边的时候直接设容量为 \(up-low\) ,提出 \(low\) . 设现在图中点的流入流出量之差为 \(c[i]\) 。
设置虚拟源汇,源点向每个 \(c[i]>0\) 的点连边,容量为 \(c[i]\) ;每个 \(c[i]<0\) 的点向汇点连边,容量为 \(-c[i]\) . 如果某个点 \(x\) 的 \(c[x]>0\) ,那么就从源点给它 \(c[x]\) 的流,这些流在原本的边上流动,直到流到某个 \(c[y]<0\) 的点 \(y\) ,通过这里流出。之后留在原有边上的流量就是附加流。
如果附加流加上初始流之后流量守恒,那么直接与虚拟源点和汇点相连的边应该都满流。由于 \(c[]\) 之和为 \(0\) ,所以新图中的最大流要等于 \(\sum [c[i]>0]c[i]\) . 如果成立那么就存在可行流。求流量就直接找每条边的反向弧即可。
//Author:RingweEH
int main()
{
n=read(); m=read(); S=n+1; T=S+1;
for ( int i=1; i<=m; i++ )
{
int u=read(),v=read(),lower=read(),upper=read();
Add( u,v,upper-lower ); c[v]+=lower; c[u]-=lower;
low[i]=lower;
}
int Sum=0;
for ( int i=1; i<=n; i++ )
{
if ( c[i]>0 ) Add( S,i,c[i] ),Sum+=c[i];
if ( c[i]<0 ) Add( i,T,-c[i] );
}
while ( BFS() ) mxflow+=DFS(S,INF);
if ( mxflow==Sum )
{
printf( "YES\n" );
for ( int i=3,j=1; j<=m; i+=2,j++ )
printf( "%lld\n",e[i].flow+low[j] );
}
else printf( "NO\n" );
return 0;
}
有源汇上下界可行流
源点和汇点流量不守恒,但是可以手动守恒:从 \(T\) 向 \(S\) 连一条下界为 \(0\) ,上界为 INF 的边,跑无源汇即可。流量就是 \(T\) 到 \(S\) 的反向弧流量。
有源汇上下界最大流
在有源汇上下界可行流的基础上,在残量网络上跑最大流,相加即可。显然这样不会超出上下界。
具体实现:用虚拟源汇跑完之后,再用实际的源汇跑跑一遍最大流即可。
原理:原图中跑完第一次,剩下的就是残量网络,且可行流保存在 \(T\) 到 \(S\) 的反向边中。这时候跑 \(S\) 到 \(T\) 的最大流,就得到了残量网络最大流+将 \(ST\) 反向边退回去的流量,也就是最大流加上可行流。
//Author:RingweEH
int main()
{
n=read(); m=read(); int s=read(),t=read(); S=n+1,T=S+1;
for ( int i=1; i<=m; i++ )
{
int u=read(),v=read(),lower=read(),upper=read(); low[i]=lower;
Add( u,v,upper-lower ); c[v]+=lower; c[u]-=lower;
}
int Sum=0; Add( t,s,INF );
for ( int i=1; i<=n; i++ )
{
if ( c[i]>0 ) Add( S,i,c[i] ),Sum+=c[i];
if ( c[i]<0 ) Add( i,T,-c[i] );
}
while ( BFS() ) mxflow+=DFS(S,INF);
if ( mxflow!=Sum ) { printf( "please go home to sleep" ); return 0; }
S=s; T=t; mxflow=0;
while ( BFS() ) mxflow+=DFS(S,INF);
printf( "%d\n",mxflow );
return 0;
}
有源汇上下界最小流
和最大流一样,先求出可行流,然后考虑在此基础上减去最大的流量,使之仍然守恒。
如果我们找到一条 \(T\to S\) 的增广路,那么就说明去掉这条增广路上的流量,仍然守恒。那么就直接从 \(T\) 到 \(S\) 跑一遍最大流,用可行流减去流量即可。注意这里 不能 保留 \(T\) 到 \(S\) 的 INF 边。
//Author:RingweEH
int main()
{
n=read(); m=read(); int s=read(),t=read(); S=n+1,T=S+1;
for ( int i=1; i<=m; i++ )
{
int u=read(),v=read(); ll lower=read(),upper=read();
low[i]=lower; Add( u,v,upper-lower ); c[v]+=lower; c[u]-=lower;
}
int Sum=0;
for ( int i=1; i<=n; i++ )
{
if ( c[i]>0 ) Add( S,i,c[i] ),Sum+=c[i];
if ( c[i]<0 ) Add( i,T,-c[i] );
}
Add( t,s,INF );
while ( BFS() ) mxflow+=DFS(S,INF);
if ( mxflow!=Sum ) { printf( "please go home to sleep" ); return 0; }
mxflow=e[tot].flow;
S=t; T=s; e[tot].flow=e[tot^1].flow=0;
while ( BFS() ) mxflow-=DFS(S,INF);
printf( "%lld\n",mxflow );
return 0;
}