网络流的用途十分广泛,下面仅仅介绍它的三种用途。
一、最大流
最大流的意思为从源点能流到汇点的最大流量,是网络流中最常见的一种。
例题1:(来源:bzoj 1066)
题意:在一个r行c列的网格地图中有一些高度不同的石柱,一些石柱上站着一些蜥蜴,你的任务是让尽量多的蜥蜴逃到边界外。 每行每列中相邻石柱的距离为1,蜥蜴的跳跃距离是d,即蜥蜴可以跳到平面距离不超过d的任何一个石柱上。石柱都不稳定,每次当蜥蜴跳跃时,所离开的石柱高度减1(如果仍然落在地图内部,则到达的石柱高度不变),如果该石柱原来高度为1,则蜥蜴离开后消失。以后其他蜥蜴不能落脚。任何时刻不能有两只蜥蜴在同一个石柱上。求无法逃离的蜥蜴总数的最小值。
构图:每根石柱拆成2个点,其中一边(石柱1)表示到达石柱的边,另一边(石柱2)表示离开石柱的边。
(st,蜥蜴所在石柱,蜥蜴数量);
(石柱1,石柱2,可以通过的蜥蜴数量);
(石柱2,下一站石柱1,INF);
当蜥蜴可以跳出地图时(石柱2,汇点,INF)。
构图的代码:
int main()
{
int ans=0;
len=0;
memset(last,0,sizeof(last));
scanf("%d%d%d",&n,&m,&d);
st=0;ed=161609;//
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
for(int j=1;j<=m;j++) map[i][j]=s[j]-'0';
}
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
for(int j=1;j<=m;j++)
{
if(s[j]=='L')
{
ins(st,i*m+j,1);
ans++;
}
}
}
for(int x1=1;x1<=n;x1++)
{
for(int y1=1;y1<=m;y1++)
{
if(map[x1][y1]>0)
{
ins(x1*m+y1,n*m+x1*m+y1,map[x1][y1]);
if(x1<=d||n-x1+1<=d||y1<=d||m-y1+1<=d) ins(n*m+x1*m+y1,ed,999999999);
for(int x2=1;x2<=n;x2++)
{
for(int y2=1;y2<=m;y2++)
{
if(map[x2][y2]>0&&d>=dis(x1*1.0,y1*1.0,x2*1.0,y2*1.0))
{
ins(n*m+x1*m+y1,x2*m+y2,999999999);
}
}
}
}
}
}
while(bfs()==true)
{
ans-=dfs(st,999999999);
}
printf("%d",ans);
return 0;
}
从中,我发现:一般如果值在点上,都要拆点。拆点后一点为入点,一点为出点。从入点连到出点,意味着该点最多能通过的流量;从一个点的出点连到另一个点的入点,意味着这两个点相通。
二、费用流
费用流在最大流的基础上,新增了选择每条边的费用,也就是说,当flow的流量从费用为v的边流过,那么会增加费用flow*w,最终,需要求出在以流到汇点的流量最大为前提下,用尽量少的费用。
费用流的结构体:
int st,ed,ans=0;
int last[ ];
struct node
{
int x,y,c,v,next,other;//c流量 v费用
}a[ ]; int len=0;
费用流的代码由 一个spfa(寻找从源点到汇点最小费用的路线)+ 一个网络流的处理函数(该变化的流量减少或增加,该费用记入ans)。
完整代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=1061109567;
const int maxn=5010,maxm=50010;
int n,m,st,ed;
struct edge
{
int x,y,c,w,next;
}e[maxm*2];int len=1,last[maxn];
void ins(int x,int y,int c,int w)
{
e[++len]=(edge){x,y,c,w,last[x]};last[x]=len;
e[++len]=(edge){y,x,0,-w,last[y]};last[y]=len;
}
int dis[maxn];//dis[x]指(流量为1时)从源点到汇点所需的最小费用(也就是最短路径)
int fr[maxn];//from[x]记录下点x是从连接点x的哪一条边来到,所以连接点x的点是a[from[x]].y,x通往汇点的下一站没有记录
int head,tail,list[maxn];bool v[maxn];
bool spfa()
{
memset(dis,63,sizeof(dis));dis[st]=0;
head=0;tail=1;
list[0]=st;v[st]=true;
while(head!=tail)
{
int x=list[head++];if(head==maxn) head=0;
v[x]=false;
for(int k=last[x];k;k=e[k].next)
{
int y=e[k].y;
if(e[k].c>0&&dis[y]>dis[x]+e[k].w)
{
dis[y]=dis[x]+e[k].w;
fr[y]=k;
if(!v[y])
{
v[y]=true;
list[tail++]=y;if(tail==maxn) tail=0;
}
}
}
}
return dis[ed]<inf;
}
int mxf,mnw;
void minf()
{
int x=ed,mn=inf,sum=0;
for(int k=fr[ed];k;k=fr[e[k].x])
{
mn=min(mn,e[k].c);
}
for(int k=fr[ed];k;k=fr[e[k].x])
{
e[k].c-=mn;e[k^1].c+=mn;
}
mxf+=mn;mnw+=mn*dis[ed];
}
int main()
{
scanf("%d%d%d%d",&n,&m,&st,&ed);
for(int i=1;i<=m;i++)
{
int x,y,c,w;
scanf("%d%d%d%d",&x,&y,&c,&w);
ins(x,y,c,w);
}
mxf=mnw=0;
while(spfa()) minf();
printf("%d %d\n",mxf,mnw);
return 0;
}
例题2:(来源:bzoj 1877)
这张地图中包含N个十字路口和M条街道(单向),Elaxia只能从一个十字路口跑向另外一个十字路口,街道之间只在十字路口处相交。Elaxia每天从寝室出发跑到学校,保证家编号为1,学校编号为N。 Elaxia的晨跑计划是按周期(包含若干天)进行的,由于他不喜欢走重复的路线,所以在一个周期内,每天的晨跑路线都不会相交(在十字路口处),家和学校不算十字路口。Elaxia耐力不太好,他希望在一个周期内跑的路程尽量短,但是又希望训练周期包含的天数尽量长
。 求最长周期的天数 和 在满足最长天数的条件下最短的总路程长度。
思路:一眼就看出,这是求从家到学校的最大流题目。但是这里还要使得路程最短,所以用费用流。
构图:
因为路口只能走一次,即有一个为1的
值在路口上,我们果断拆点(路口1,路口2,1,0);
如果两个十字路口相通(路的起点 路口2,
路的终点
路口1,1,路长)。
例题3:(来源:bzoj 1070)
题意:同一时刻有n位车主带着他们的爱车来到了汽车维修中心。维修中心共有m位技术人员,不同的技术人员对不同的车进行维修所用的时间是不同的。现在需要安排这m位技术人员所维修的车及顺序,使得顾客平均等待的时间最小。 说明:顾客的等待时间是指从他把车送至维修中心到维修完毕所用的时间,车只能一辆一辆地修。
构图:给技术人员拆点,拆成n个,每个技术人员的第k个点表示该技术人员在倒数第k个修理这辆车
,而其它的车主 等了k*t的时间
。
(st,车,1,0);
(车,技术人员,1,其余车主等的时间的花费);
(技术人员,ed,1,0)。
它的构图神奇在于给技术人员拆点,使某个技术人员 在何时 修理何辆车 的所有情况得以表现。
构图代码:
int main()
{
int n,m;
scanf("%d%d",&m,&n);
st=0;ed=n+n*m+1;
for(int i=1;i<=n;i++)
{
ins(st,i,1,0);
for(int j=1;j<=m;j++)
{
int t;
scanf("%d",&t);
for(int k=1;k<=n;k++)
{
ins(i,j*n+k,1,k*t);
}
}
}
for(int i=n+1;i<ed;i++)
{
ins(i,ed,1,0);
}
while(spfa()==true)
{
minf();
}
printf("%.2lf",(double)ans/n);
return 0;
}
三、最小割
最小割的含义:割去一些边,使得从st出发的流量无法到达ed,同时使总容量最小。根据大神们的证明:在一个网络流中,最小割等于最大流。
例题4:(来源:bzoj 1934)
题意:幼儿园里有n个小朋友打算通过投票来决定睡不睡午觉。对他们来说,这个问题并不是很重要,于是他们决定发扬谦让精神。虽然每个人都有自己的主见,但是为了照顾一下自己朋友的想法,他们也可以投和自己本来意愿相反的票。我们定义一次投票的冲突数为好朋友之间发生冲突的总数加上和所有和自己本来意愿发生冲突的人数。 我们的问题就是,每位小朋友应该怎样投票,才能使冲突数最小?
构图:(st,选择睡觉的小朋友,INF,0);
(选择不睡觉的小朋友,ed,INF,0);
(选择睡觉的小朋友,选择不睡觉的小朋友,1,1)。
例题5:(来源:bzoj 2039)
题意:作为一个富有经营头脑的富翁,小L决定从本国最优秀的经理中雇佣一些来经营自己的公司。这些经理相互之间合作有一个贡献指数,(我们用Ei,j表示i经理对j经理的了解程度),即当经理i和经理j同时被雇佣时,经理i会对经理j做出贡献,使得所赚得的利润增加2*Ei,j。当然,雇佣每一个经理都需要花费一定的金钱Ai,对于一些经理可能他做出的贡献不值得他的花费,那么作为一个聪明的人,小L当然不会雇佣他。 然而,那些没有被雇佣的人会被竞争对手所雇佣,这个时候那些人会对你雇佣的经理的工作造成影响,使得所赚得的利润减少2*Ei,j(注意:这里的Ei,j与上面的Ei,j 是同一个)。 作为一个效率优先的人,小L想雇佣一些人使得净利润最大。你可以帮助小L解决这个问题吗?
思路:最小割适用于求“最小”,如果遇到最大选择方案,可以将其转化为最小后,再用最小割求解。(如求最大利润可以转化为求最小花费)下面的构图,用的全是减法。
构图:不选择经理(st,经理,能获得的费用,0);
两经理发生矛盾(经理1,经理2,合作的利益+冲突的损失,合作的利益+冲突的损失);
选择经理(经理,ed,经理所需工资,0)。
另外,这里给从源点给每个经理连的边太多了,我们要进行合边,以减少数据存储量和时间。
构图代码:
int main()
{
int n,sum=0;
scanf("%d",&n);
st=0;ed=n+1;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
ins(i,ed,x,0);
}
for(int i=1;i<=n;i++)
{
int s=0;
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
s+=x;sum+=x;
if(i<j) ins(i,j,2*x,2*x);
}
ins(st,i,s,0);
}
while(bfs()==true)
{
ans+=dfs(st,999999999);
}
printf("%d",sum-ans);
return 0;
}
综上,我们不难发现,最小割的构图思想与其上的两种有些微妙的区别,这里有必要归纳一下最小割的基本构图方法:
1、把一堆的点分成两个点集,一堆从st流过来,另一堆流向ed,中间的点允许随意相连;
2、最小割每割去一条边,表示选择了该边的情况,这是单向边的意义。所以,如果某条边不能被割去,就给它个最大值;
3、在最小割构图中,经常会遇到要建双向边的情况。在两者之间构建双向边,意为:(1)若这两点的情况相同(均被雇佣或均不被雇佣),则不需要选择相连的边;(2)如果不同(一被雇佣,一不被雇佣),则要必须选择其中一条边。
4、所以,如果遇到当两者选择不同情况 会发生冲突(增加新值),而选择相同情况正常时,考虑构建双向边。
文章最后,送给亲爱的读者一篇很有深度的论文和一个bzoj网络流的刷题的题表推荐。
论文:http://images2015.cnblogs.com/blog/697201/201601/697201-20160120212611453-314392413.png
题表:http://www.cnblogs.com/yzcstc/p/4055317.html