[整理]网络流随记——中(费用流)

前篇:最大流

0.概述

我们在之前已经探讨了网络最大流的定义、两种求法及应用,如果有不了解的同学可以翻阅我之前的博客。
而费用流(最小费用最大流)与普通最大流的区别就是它在每条边上加了一个单位流量的费用,需要在满足最大流的同时满足费用最小。
百度百科(比最大流还不像人话,建议跳过)
费用流的应用比最大流更加广泛,所以我们将在这一期探讨费用流的求法及少量应用。

1. SSP 算法

SSP(Successive Shortest Path)算法:在最大流的 EK 算法求解最大流的基础上,把 用 BFS 求解任意增广路 改为 用 SPFA 求解单位费用之和最小的增广路 即可。—— OI Wiki

通俗地讲,所谓 SSP 算法,就是 EK + SPFA 。
那么它是根据什么原理来实现的呢?我们来分析一下(作者才疏学浅如有失误敬请指出)。
这种算法其实是利用了贪心的思想:我们每次利用 SPFA 求费用的一条最短路,实际上就是保证了在这条路上走的花费是最小的。
接下来我们按照 EK 算法的思路,不断寻找增广路,最终一定可以得到一个答案且这个答案为最大流。那么此时由于每条路径上的费用都是最小的,总费用也一定是最小的,证毕。
代码实现很简单,把 BFS 改为 SPFA ,然后记录一下最短路径即可。
洛谷P3381 【模板】最小费用最大流 核心代码:

#define N 5010
#define M 50010
int n,m,s,t,ans,ansflow;
int dis[N],lst[N],vis[N];
struct Edge {
	int to,nxt,flow,wei;
}e[M<<1];
int hd[N],cnt=-1;
il void ade(int u,int v,int w,int c){
	e[++cnt].to=v,e[cnt].flow=w;
	e[cnt].wei=c,e[cnt].nxt=hd[u],hd[u]=cnt;
}
il bool SPFA(){//SPFA求费用的一条最短路
	memset(dis,0x3f,sizeof(dis));
	memset(lst,0,sizeof(lst));
	memset(vis,0,sizeof(vis));
	queue<int>q;
	q.push(s),vis[s]=1,dis[s]=0;
	while(!q.empty()){
		int u=q.front();q.pop(),vis[u]=0;
		for(rg int i=hd[u];~i;i=e[i].nxt){
			int v=e[i].to;
			if(dis[v]>dis[u]+e[i].wei&&e[i].flow){//在SPFA的基础上增加了一条能作为增广路的判断(还有流量)
				dis[v]=dis[u]+e[i].wei,lst[v]=i;//lst用于记录路径
				if(!vis[v]){
					q.push(v),vis[v]=1;
				}
			}
		}
	}
	return dis[t]!=INF;//能不能找到一条增广路
}
il int Update(int &flowout){//与普通EK的代码实现几乎一样
	int mxflow=INF,res=0,i;
	for(rg int u=t;u!=s;u=e[i^1].to){
		i=lst[u],mxflow=min(mxflow,e[i].flow);
	}
	for(rg int u=t;u!=s;u=e[i^1].to){
		i=lst[u];
		e[i].flow-=mxflow,e[i^1].flow+=mxflow;
		res+=e[i].wei*mxflow;//记录费用
	}
	flowout+=mxflow;
	return res;
}
void EK(int &flowout){
	while(SPFA()){
		ans+=Update(flowout);
	}
}
int main(){
	Read(n),Read(m),Read(s),Read(t);
	memset(hd,-1,sizeof(hd));
	for(rg int i=1,u,v,w,c;i<=m;i++){
		Read(u),Read(v),Read(w),Read(c);
		ade(u,v,w,c),ade(v,u,0,-c);//注意反向边的费用是负的
	}
	EK(ansflow);
	cout<<ansflow<<" "<<ans<<endl;
	return 0;
}

从代码中可以看出,费用图中的某些边权可能是负数,所以我们需要使用 SPFA 算法求最短路。(虽然有解决方案但是应该没有什么出题人会在这里卡 SPFA 罢)
另外,求费用流还有一种类 Dinic 算法,大家可以自行查找资料学习。

2.应用

与最大流一样,费用流也有多种有(du)趣(liu)的应用。
例题一:洛谷P4014 分配问题
上期我们讲了如何将二分图最大匹配转为最大流,今天我们将解决带权匹配的情况。
可以看出这道题是一个带权二分图完美匹配,可以用费用流来解决。
具体地说,从超级源点向每个人连流量为1、费用为0的边,从每个工作向超级汇点连流量为1、费用为0的边,表示一个人匹配一个工作。
然后从每个人向每个工作连流量为1、费用为c[i][j]的边,表示走了这条路会增加c[i][j]的效益。
此时在图上求超级源点到超级汇点的费用流即可求出最小效益。
最大效益把费用改成负数即可,记得答案也要取反。
垃圾代码:

#define N 110
int n,s,t,c[N][N],ans,flowout;
int dis[N<<1],lst[N<<1],vis[N<<1];
struct Edge {
	int to,nxt,flow,wei;
}e[N*N+N+N];
int hd[N<<1],cnt=-1;
il void ade(int u,int v,int c,int w){
	e[++cnt].to=v,e[cnt].flow=c,e[cnt].wei=w;
	e[cnt].nxt=hd[u],hd[u]=cnt;
}
il bool SPFA(){
	memset(dis,0x3f,sizeof(dis));
	memset(lst,0,sizeof(lst));
	memset(vis,0,sizeof(vis));
	queue<int>q;
	q.push(s),dis[s]=0,vis[s]=1;
	while(!q.empty()){
		int u=q.front();q.pop(),vis[u]=0;
		for(rg int i=hd[u];~i;i=e[i].nxt){
			int v=e[i].to;
			if(dis[v]>dis[u]+e[i].wei&&e[i].flow){
				dis[v]=dis[u]+e[i].wei,lst[v]=i;
				if(!vis[v]){
					q.push(v),vis[v]=1;
				}
			}
		}
	}
	return dis[t]!=INF;
}
il int Update(int &flowout){
	int mxflow=INF,res=0,i;
	for(rg int u=t;u!=s;u=e[i^1].to){
		i=lst[u],mxflow=min(mxflow,e[i].flow);
	}
	for(rg int u=t;u!=s;u=e[i^1].to){
		i=lst[u];
		e[i].flow-=mxflow,e[i^1].flow+=mxflow;
		res+=mxflow*e[i].wei;
	}
	flowout+=mxflow;
	return res;
}
il void EK(int &flowout){
	while(SPFA()){
		ans+=Update(flowout);
	}
}
int main(){
	memset(hd,-1,sizeof(hd));
	Read(n);
	for(rg int i=1;i<=n;i++){
		for(rg int j=1;j<=n;j++)Read(c[i][j]);
	}
	s=0,t=2*n+1;//超级源点和超级汇点
	for(rg int i=1;i<=n;i++)ade(s,i,1,0),ade(i,s,0,0);//第一轮连边
	for(rg int i=n+1;i<=n+n;i++)ade(i,t,1,0),ade(t,i,0,0);
	for(rg int i=1;i<=n;i++){
		for(rg int j=1;j<=n;j++){
			ade(i,n+j,1,c[i][j]),ade(n+j,i,0,-c[i][j]);
		}
	}
	EK(flowout);
	cout<<ans<<endl;
	memset(hd,-1,sizeof(hd)),cnt=-1,ans=flowout=0;
	for(rg int i=1;i<=n;i++)ade(s,i,1,0),ade(i,s,0,0);//第二轮连边
	for(rg int i=n+1;i<=n+n;i++)ade(i,t,1,0),ade(t,i,0,0);
	for(rg int i=1;i<=n;i++){
		for(rg int j=1;j<=n;j++){
			ade(i,n+j,1,-c[i][j]),ade(n+j,i,0,c[i][j]);
		}
	}//感觉写两次太丑了...但是为了方便就先这样吧
	EK(flowout);
	cout<<-ans<<endl;
	return 0;
}

例题二:洛谷P2517 [HAOI2010]订货
这道题可以用 dp 等其他解法解决,但我们现在来讲解网络流的做法。
仔细分析题目可以得出三个条件:
1.每个月可以买入任意多产品,单价为d[i]
2.每个月可以卖出至多U[i]的产品,无代价;
3.每个月可以向下个月储存至多S的产品,单价为m
那么我们按照这三个条件建图,超级源点连每个月,每个月连下个月,每个月连超级汇点。
代码即为ade(s,i,INF,d[i])ade(i,t,U[i],0)ade(i,i+1,S,m)(没有写出反向边)。
此时求费用流即得答案,一条流可以看成一个产品从买入到卖出的经历。
垃圾代码:

#define N 60
int n,m,S,U[N],d[N],s,t,ans,flowout;
int dis[N],lst[N],vis[N];
struct Edge {
	int to,nxt,flow,wei;
}e[N<<3];
int hd[N],cnt=-1;
il void ade(int u,int v,int c,int w){
	e[++cnt].to=v,e[cnt].flow=c,e[cnt].wei=w;
	e[cnt].nxt=hd[u],hd[u]=cnt;
}
il bool SPFA(){
	memset(dis,0x3f,sizeof(dis));
	memset(lst,0,sizeof(lst));
	memset(vis,0,sizeof(vis));
	queue<int>q;
	q.push(s),dis[s]=0,vis[s]=1;
	while(!q.empty()){
		int u=q.front();q.pop(),vis[u]=0;
		for(rg int i=hd[u];~i;i=e[i].nxt){
			int v=e[i].to;
			if(dis[v]>dis[u]+e[i].wei&&e[i].flow){
				dis[v]=dis[u]+e[i].wei,lst[v]=i;
				if(!vis[v]){
					q.push(v),vis[v]=1;
				}
			}
		}
	}
	return dis[t]!=INF;
}
il int Update(int &flowout){
	int mxflow=INF,res=0,i;
	for(rg int u=t;u!=s;u=e[i^1].to){
		i=lst[u],mxflow=min(mxflow,e[i].flow);
	}
	for(rg int u=t;u!=s;u=e[i^1].to){
		i=lst[u];
		e[i].flow-=mxflow,e[i^1].flow+=mxflow;
		res+=mxflow*e[i].wei;
	}
	flowout+=mxflow;
	return res;
}
il void EK(int &flowout){
	while(SPFA()){
		ans+=Update(flowout);
	}
}
il void Build(){
	for(rg int i=1;i<=n;i++)ade(s,i,INF,d[i]),ade(i,s,0,-d[i]);
	for(rg int i=1;i<=n;i++)ade(i,t,U[i],0),ade(t,i,0,0);
	for(rg int i=1;i<n;i++)ade(i,i+1,S,m),ade(i+1,i,0,-m);
}
int main(){
	memset(hd,-1,sizeof(hd));
	Read(n),Read(m),Read(S);
	for(rg int i=1;i<=n;i++)Read(U[i]);
	for(rg int i=1;i<=n;i++)Read(d[i]);
	s=0,t=n+1;
	Build();
	EK(flowout);
	cout<<ans<<endl;
	return 0;
}

3.总结

费用流也是网络流的一种,与最大流一样可以用来解决一些不那么像网络流的问题,本文只提到了其应用的冰山一角,下一篇文章里我们将会深入探讨网络流的各种优化及建模方式。

4.练习题

网络流24题
洛谷P2469 [SDOI2010]星际竞速

posted @ 2020-12-27 09:45  ajthreac  阅读(750)  评论(0编辑  收藏  举报