zoukankan      html  css  js  c++  java
  • [整理]网络流随记——中(费用流)

    前篇:最大流

    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]星际竞速

  • 相关阅读:
    N皇后问题
    SDNU1349.快速幂入门
    SDNU1522.陆历川学数学
    埃氏筛
    快速幂
    string函数
    Golang介绍以及安装
    Promise解决回调地狱(多层调用问题)
    JavaScript动画相关
    ES6简单语法
  • 原文地址:https://www.cnblogs.com/juruoajh/p/14195463.html
Copyright © 2011-2022 走看看