猴子课堂:最小费用最大流

Posted zhangjianjunab

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了猴子课堂:最小费用最大流相关的知识,希望对你有一定的参考价值。

哦末,刚学了费用流,就来造福人民,哈哈,大佬勿喷(其实是GDOI爆零,心情不好写一篇博客安慰自己)

技术分享图片技术分享图片?技术分享图片技术分享图片?

好了,回归正题,这里我只讲费用流的两种方法(没有负权环)

原题模版

一样的,建反向边,然后就开始操作了!

首先,是MCMF费用流,即连续用SPFA计算从起点到终点的最小费用,在进行SPFA中顺带记录流量、前一个点、前一条边,然后,就。。。从终点回去更新一路到起点就行了,先给出SPFA(为什么会对?找最短费用,然后还有反向边后悔,为什么不行?):

        memset(v,false,sizeof(v));v[st]=true;/*统计这个数是否在队列*/
	head=1;tail=2;list[1]=st;/*从前点开始*/
	memset(dis,63,sizeof(dis));dis[st]=0;/*最小费用,从起点开始*/
	b[ed]=-1;/*还要判断是否可行,b代表的前一条边,不太理解看后面*/
	while(head!=tail)   
	{
		int  x=list[head];
		for(int  k=last[x];k;k=a[k].next)/*边目录存*/
		{
			int  y=a[k].y;
			if(a[k].c>0  &&  dis[x]+a[k].k<dis[y])/*看看这条可行边是否可以更新y点*/
			{
				dis[y]=dis[x]+a[k].k;/*更新*/
				flow[y]=mymin(a[k].c,flow[x]);/*更新最多能到达的流量*/
				qian[y]=x;/*更新到达y点的点*/b[y]=k;/*同时更新边*/
				if(v[y]==false)/*扔进队列*/
				{
					v[y]=true;
					list[tail++]=y;
					if(tail==n+1)tail=1;
				}
			}
		}
		head++;
		if(head==n+1)head=1;
		v[x]=false;/*找完后将他改为false*/
	}

  

技术分享图片

下面给出将边修改的过程:

	if(b[ed]!=-1)/*存在流时进来*/
	{
		int  y=ed,root=0;/*更新边流量的过程*/
		while(y!=st)
		{
			root=b[y];y=qian[y];/*找出这条最小费用路径的边和点*/
			a[root].c-=flow[ed];
			a[a[root].other].c+=flow[ed];/*边处理*/
		}
		zans+=flow[ed];
		cost+=flow[ed]*dis[ed];/*更新花费与流量*/
	}
	return  b[ed]!=-1;//返回bool值 

  

 

然后,吗。。。再来个主函数(感觉好鸡肋得主函数):

 

int  main()
{
	scanf("%d%d",&n,&m);
	st=1;ed=n;
	for(register  int  i=1;i<=m;i++)
	{
		int  x,y;
		ll  z,k;
		scanf("%d%d%lld%lld",&x,&y,&z,&k);
		ins(x,y,z,k);
	}
	flow[st]=ll(999999999999999);/*初始化起点有无数的流量*/ 
	while(spfa()==true);//一直到不存在路径为止。 
	printf("%lld %lld",zans,cost);/*输出*/
	return  0;
}

  

技术分享图片

ZKW大佬优化后的ZKW牌费用流(祛风除湿止痛费用流哟,年轻人):

 

大佬的方法就是找到多条增广路,用SPFA找到多条增广路(从终点开始计算(然而从起点开始好像也无所谓啦!但是,作者用自己的亲身试验证明,会慢!╯﹏╰,不知为什么))。

如图:

技术分享图片技术分享图片?

这张图,明显用MCMF要两次SPFA,但是,SPFA的特点是可以计算出所有点离起点有多远!所以,咱们可以用一次递归来找出所有最短路径。

 

ZKW费用流的SPFA

int  list[1100],head,tail;/*队列*/
inline  bool  spfa()
{
    memset(v,false,sizeof(v));v[ed]=true;/*判断是否进入队列*/
    memset(d,-1,sizeof(d));d[ed]=0;/*从终点到这里要多少费用*/
    head=1;tail=2;list[head]=ed;/*从终点出发*/
    while(head!=tail)
    {
        int  x=list[head];
        for(int  k=last[x];k;k=a[k].next)
        {
            if(a[a[k].other].c>0/*由于是倒着搜的,所以边也要反向边*/  &&  (a[a[k].other].k+d[x]<d[a[k].y]  ||  d[a[k].y]==-1))/*判断边是否可行并更新*/
            {
                d[a[k].y]=a[a[k].other].k+d[x];/*更新*/
                int  y=a[k].y;
                if(v[y]==false)
                {
                    v[y]=true;
                    list[tail]=y;
                    tail++;
                    if(tail==n+1)tail=1;/*这里可以用SLF优化*/
                }
            }
        }
        head++;
        if(head==n+1)head=1;
        v[x]=false;
    }
    return  d[st]!=-1;/*返回bool值*/
}

  

 

inline  ll  mymin(ll  x,ll  y){return  x<y?x:y;}/*找最小值*/
long  long  find(int  x,ll  f)
{
    v[x]=true;
    if(x==ed)return  f;
    ll  ans=0,t=0;
    for(int  k=last[x];k;k=a[k].next)
    {
        int  y=a[k].y;
        if(v[y]==false/*这个点在这条路径没走过才可以走,否则。。。Balabala*/  &&  a[k].c>0  &&  d[x]-a[k].k==d[y]/*类似分层的操作*/  &&  ans<f)
        {
            ans+=t=find(y,mymin(a[k].c,f-ans));/*是不是很眼熟?*/
            a[k].c-=t;a[a[k].other].c+=t;cost+=t*a[k].k;
        }
    }
    return  ans;/*妥妥的像最大流*/
}

  

技术分享图片

 

牛逼的!主函数:

int  main()
{
    scanf("%d%d",&n,&m);
    st=1;ed=n;
    for(int  i=1;i<=m;i++)
    {
        int  x,y;
        ll  z,l;
        scanf("%d%d%lld%lld",&x,&y,&z,&l);
        ins(x,y,z,l);
    }
    ll  zans=0;
    while(spfa()==true)/*建图完成!*/
    {
        do
        {
            memset(v,false,sizeof(v));
            zans+=find(st,ll(999999999999999));/*多次查找,找出所有增光路哦*/
        }while(v[ed]==true);/*走不到终点,退出!*/
    }
    printf("%lld %lld",zans,cost);
    return  0;
}

  

技术分享图片

 

然而。。。

 

还可以更优!

细心的同学可以发现了一个尴尬的情况:

技术分享图片技术分享图片?

但是:

技术分享图片技术分享图片?

于是,当找到t>0时,break;就可以了!

inline  ll  mymin(ll  x,ll  y){return  x<y?x:y;}
long  long  find(int  x,ll  f)
{
    v[x]=true;
    if(x==ed)return  f;
    ll  ans=0,t=0;
    for(int  k=last[x];k;k=a[k].next)
    {
        int  y=a[k].y;
        if(v[y]==false  &&  a[k].c>0  &&  d[x]-a[k].k==d[y]  &&  ans<f)
        {
            ans+=t=find(y,mymin(a[k].c,f-ans));
            a[k].c-=t;a[a[k].other].c+=t;cost+=t*a[k].k;
            if(t!=0)break;
        }
    }
    return  ans;
}

  

但是,你又会发现,不断的break,多次递归,太慢了。

技术分享图片技术分享图片?

于是,你可以将递归加个回溯,这就快多了:

find函数:

inline  ll  mymin(ll  x,ll  y){return  x<y?x:y;}/*找最小值*/
long  long  find(int  x,ll  f)
{
    v[x]=true;
    if(x==ed){v[x]=false;return  f;}
    ll  ans=0,t=0;
    for(int  k=last[x];k;k=a[k].next)
    {
        int  y=a[k].y;
        if(v[y]==false/*这个点没走过才可以走,否则更新边的流量是会Balabala*/  &&  a[k].c>0  &&  d[x]-a[k].k==d[y]/*类似分层的操作*/  &&  ans<f)
        {
            ans+=t=find(y,mymin(a[k].c,f-ans));/*是不是很眼熟?*/
            a[k].c-=t;a[a[k].other].c+=t;cost+=t*a[k].k;
        }
    }
    v[x]=false;/*回溯*/
    return  ans;/*妥妥的像最大流*/
}

  

技术分享图片

主函数:

int  main()
{
    scanf("%d%d",&n,&m);
    st=1;ed=n;
    for(int  i=1;i<=m;i++)
    {
        int  x,y;
        ll  z,l;
        scanf("%d%d%lld%lld",&x,&y,&z,&l);
        ins(x,y,z,l);
    }
    ll  zans=0;
    while(spfa()==true)/*建图完成!*/
    {
        zans+=find(st,ll(999999999999999));/*开心,一次就够!*/
    }
    printf("%lld %lld",zans,cost);
    return  0;
}

  

技术分享图片

技术分享图片技术分享图片?

 

最后,贴上整个代码,祝大家学会网络流:

 

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  ll;
struct  node
{
    int  y,next,other;
    ll  c,k;
}a[201000];int  last[1000],len;
long  long  d[1100];
bool  v[1100];
int  n,m,st,ed;
ll  cost=0;
inline void  ins(int  x,int  y,ll  c,ll  k)
{
    len++;
    a[len].y=y;a[len].c=c;a[len].k=k;
    a[len].next=last[x];last[x]=len;
    len++;
    a[len].y=x;a[len].c=0;a[len].k=-k;
    a[len].next=last[y];last[y]=len;
    a[len-1].other=len;
    a[len].other=len-1;
}
int  list[1100],head,tail;/*队列*/
inline  bool  spfa()
{
    memset(v,false,sizeof(v));v[ed]=true;/*判断是否进入队列*/
    memset(d,-1,sizeof(d));d[ed]=0;/*从终点到这里要多少费用*/
    head=1;tail=2;list[head]=ed;/*从终点出发*/
    while(head!=tail)
    {
        int  x=list[head];
        for(int  k=last[x];k;k=a[k].next)
        {
            if(a[a[k].other].c>0/*由于是倒着搜的,所以边也要反向边*/  &&  (a[a[k].other].k+d[x]<d[a[k].y]  ||  d[a[k].y]==-1))/*判断边是否可行并更新*/
            {
                d[a[k].y]=a[a[k].other].k+d[x];/*更新*/
                int  y=a[k].y;
                if(v[y]==false)
                {
                    v[y]=true;
                    list[tail]=y;
                    tail++;
                    if(tail==n+1)tail=1;
                }
            }
        }
        head++;
        if(head==n+1)head=1;
        v[x]=false;
    }
    return  d[st]!=-1;/*返回bool值*/
}
inline  ll  mymin(ll  x,ll  y){return  x<y?x:y;}/*找最小值*/
long  long  find(int  x,ll  f)
{
    v[x]=true;
    if(x==ed){v[x]=false;return  f;}
    ll  ans=0,t=0;
    for(int  k=last[x];k;k=a[k].next)
    {
        int  y=a[k].y;
        if(v[y]==false/*这个点没走过才可以走,否则更新边的流量是会Balabala*/  &&  a[k].c>0  &&  d[x]-a[k].k==d[y]/*类似分层的操作*/  &&  ans<f)
        {
            ans+=t=find(y,mymin(a[k].c,f-ans));/*是不是很眼熟?*/
            a[k].c-=t;a[a[k].other].c+=t;cost+=t*a[k].k;
        }
    }
    v[x]=false;
    return  ans;/*妥妥的像最大流*/
}
int  main()
{
    scanf("%d%d",&n,&m);
    st=1;ed=n;
    for(int  i=1;i<=m;i++)
    {
        int  x,y;
        ll  z,l;
        scanf("%d%d%lld%lld",&x,&y,&z,&l);
        ins(x,y,z,l);
    }
    ll  zans=0;
    while(spfa()==true)/*建图完成!*/
    {
        zans+=find(st,ll(999999999999999));/*多次查找,找出所有增光路哦*/
    }
    printf("%lld %lld",zans,cost);
    return  0;
}

  

最后,总结一下,稀疏图用MCMF会快(很暴力,我喜欢!),稠密图还是用ZKW费用流吧(多条的增光路哟,年轻人!)

技术分享图片技术分享图片?技术分享图片技术分享图片?技术分享图片技术分享图片?

(大佬表示:垃圾,没证明)(萌新表示:垃圾,看不懂!)(我:但是我可以秀图!)

 

 

注:上面的图片侵权抱歉!

以上是关于猴子课堂:最小费用最大流的主要内容,如果未能解决你的问题,请参考以下文章

最小费用最大流算法

模板最小费用最大流

[洛谷P3381]模板最小费用最大流

最小费用最大流问题

POJ 2195 & HDU 1533 Going Home(最小费用最大流)

费用流伪代码