前篇:最大流
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
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< 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 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< 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< 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 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 } 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< return 0; } 3.总结 费用流也是网络流的一种,与最大流一样可以用来解决一些不那么像网络流的问题,本文只提到了其应用的冰山一角,下一篇文章里我们将会深入探讨网络流的各种优化及建模方式。 4.练习题 网络流24题 洛谷P2469 [SDOI2010]星际竞速