网络流学习笔记

1 - 最大流

下面那个什么能跑 \(1e4,1e5\) 的是前人经验,没拿数据测试过,不过一般网络流题的复杂度好像是玄学,数据范围算出来不是太离谱都能做……

问题概述

基本概念参见 OI-Wiki

有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点)。

高科技:O(nm)最大流

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)\) ,但是需要一些优化才能在随机数据下和增广路算法相当。

  1. 可以通过一边 BFS 将每个点的初始高度设为到汇点的距离。(除了 \(S\)
  2. 如果一个点被重贴标签之后,原来的高度已经没有点了,那么高于它的点必然不能推到 \(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;
}
posted @ 2021-01-09 15:01  MontesquieuE  阅读(350)  评论(0编辑  收藏  举报