莫名其妙就一百粉了(
搬一篇我的博文证明我还活着((
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;
}
进我的收藏夹吃灰去吧吃灰优良传统%%%%%%%
dinic的时间复杂度是$\text{O}(n^2m)$,而且迫真处理范围$1e4~1e5$
我在写什么边数 $1e4$ 的话应该是随便跑跑吧(复杂度玄学/kel