zoukankan      html  css  js  c++  java
  • 最小费用最大流模板

    笔者在写作这篇笔记之前做了整整两天的最大流,然后。。。发现网络流24题里有很多怎么看都是不可做的题目,于是solution了一把,发现要去切一下费用流这个东东,于是借鉴各种blog和题解,现在勉强搞懂了这个东西,所以作一篇笔记聊以记录和日后复习。

    如果您还没有学习网络流的基本概念,请出门左转百度吧。。(事实是笔者太懒只整理了题目而没有详解)。

    首先,先明白费用流是什么:

    费用流建立在网络最大流的基础上,一张图中最大流有且仅有一个,但是最大流条数往往不止一条,这时候对于我们来说,可能要找出这些最大流中最小(或者最大)的那一条路径,这就是最小(最大)费用最大流 。

    实现的基本思想:给出一张网络,那么这个网络的最大流一定是个定值(即使它有多种方法实现这个最大值),我们只要从当前可行流开始增广的时候,选择费用最少的一条路径就可以了。

    我们有多种方法实现最小费用的计算:

    其一:最原始的E-K算法+spfa。

    这个自然没什么说的(如果您会了最大流却不知道spfa,那我也只能说您666),将弧的费用看做是路径长度,即可转化为求最短路的问题了。只需要所走的最短路满足两个条件即可:一个是残量不为0,一个是最短路的那个条件。建图的方法依题来建,不过大体建出来和DINIC差不多。

    以洛谷P3381的模板题为例:

      1 #include<iostream>
      2 #include<cstdio>
      3 #include<cstring>
      4 #include<algorithm>
      5 #include<cmath>
      6 #include<queue>
      7 #define ll long long
      8 #define inf 50000000
      9 #define re register
     10 using namespace std;
     11 struct po
     12 {
     13     int from,to,dis,nxt,w;
     14 }edge[250001];
     15 int head[250001],cur[1000001],dep[60001],n,m,s,t,u,num=-1,x,y,l,tot,sum,k,fa[10001];
     16 int dis[5001],b[5001],xb[5001],flow[5001];
     17 inline int read()
     18 {
     19     int x=0,c=1;
     20     char ch=' ';
     21     while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
     22     while(ch=='-')c*=-1,ch=getchar();
     23     while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
     24     return x*c;
     25 }
     26 inline void add_edge(int from,int to,int w,int dis)
     27 {
     28     edge[++num].nxt=head[from];
     29     edge[num].from=from;
     30     edge[num].to=to;
     31     edge[num].w=w;
     32     edge[num].dis=dis;
     33     head[from]=num;
     34 }
     35 inline void add(int from,int to,int w,int dis)
     36 {
     37     add_edge(from,to,w,dis);
     38     add_edge(to,from,0,-dis);
     39 }
     40 inline bool bfs()
     41 {
     42     memset(dis,100,sizeof(dis));
     43     memset(b,0,sizeof(b));
     44     queue<int> q;
     45     while(!q.empty())
     46     q.pop();
     47     for(re int i=1;i<=n;i++)
     48     {
     49         fa[i]=-1;
     50     }
     51     b[s]=1;dis[s]=0;fa[s]=0;
     52     flow[s]=inf;q.push(s);
     53     while(!q.empty())
     54     {
     55         int u=q.front();
     56         q.pop();
     57         b[u]=0;
     58         for(re int i=head[u];i!=-1;i=edge[i].nxt)
     59         {
     60             int v=edge[i].to;
     61             if(edge[i].w>0&&dis[v]>dis[u]+edge[i].dis)
     62             {
     63                 dis[v]=dis[u]+edge[i].dis;
     64                 fa[v]=u;
     65                 xb[v]=i;
     66                 flow[v]=min(flow[u],edge[i].w);
     67                 if(!b[v]){b[v]=1,q.push(v);}
     68             }
     69         }
     70     }
     71     return dis[t]<inf;
     72 }
     73 inline void max_flow()
     74 {
     75     while(bfs())
     76     {
     77         int k=t;
     78         while(k!=s)
     79         {
     80             edge[xb[k]].w-=flow[t];
     81             edge[xb[k]^1].w+=flow[t];
     82             k=fa[k];
     83         }
     84         tot+=flow[t];
     85         sum+=flow[t]*dis[t];
     86     }
     87 }
     88 int main()
     89 {
     90     memset(head,-1,sizeof(head));
     91     n=read();m=read();s=read();t=read();
     92     for(re int i=1;i<=m;i++)
     93     {
     94         x=read();y=read();l=read();
     95         int d=read();
     96         add(x,y,l,d);
     97     }
     98     max_flow();
     99     cout<<tot<<" "<<sum;
    100 }

    可以看出,虽然笔者的常数优化比较优秀,然而还是卡着时间跑过最后两个点。

    其二:zkw费用流

    这个方法笔者没有深究,仔细阅读之后发现要使zkw费用流算法达到最好的效率,那么必须使用KM算法。然而不会KM算法怎么办,您可以跳过这个部分,直接看第三种实现方式。

    原始的EK算法虽然在做题的时候不会出很大的问题——因为现在的费用流的题目数据都不是很大。它的缺点比较明显:在增广的时候单路增广,导致速度减慢。zkw大神于是乎发明了一种可以直接多路增广的算法,并用KM算法节省了spfa或者迪杰斯特拉的时间,然而悲催的是,笔者自己也对KM算法不是很理解,抱歉无法写出程序比较一下时空复杂度。不过为了保持博文的完整性,还是放上方法发明者zkw的钦定标程:

    #include <cstdio>
    #include <cstring>
    using namespace std;
    const int maxint=~0U>>1;
    
    int n,m,pi1,cost=0;
    bool v[550];
    struct etype
    {
        int t,c,u;
        etype *next,*pair;
        etype(){}
        etype(int t_,int c_,int u_,etype* next_):
            t(t_),c(c_),u(u_),next(next_){}
        void* operator new(unsigned,void* p){return p;}
    } *e[550];
    
    int aug(int no,int m)
    {
        if(no==n)return cost+=pi1*m,m;
        v[no]=true;
        int l=m;
        for(etype *i=e[no];i;i=i->next)
            if(i->u && !i->c && !v[i->t])
            {
                int d=aug(i->t,l<i->u?l:i->u);
                i->u-=d,i->pair->u+=d,l-=d;
                if(!l)return m;
            }
        return m-l;
    }
    
    bool modlabel()
    {
        int d=maxint;
        for(int i=1;i<=n;++i)if(v[i])
            for(etype *j=e[i];j;j=j->next)
                if(j->u && !v[j->t] && j->c<d)d=j->c;
        if(d==maxint)return false;
        for(int i=1;i<=n;++i)if(v[i])
            for(etype *j=e[i];j;j=j->next)
                j->c-=d,j->pair->c+=d;
        pi1 += d;
        return true;
    }
    
    int main()
    {
        freopen("costflow.in","r",stdin);
        freopen("costflow.out","w",stdout);
        scanf("%d %d",&n,&m);
        etype *Pe=new etype[m+m];
        while(m--)
        {
            int s,t,c,u;
            scanf("%d%d%d%d",&s,&t,&u,&c);
            e[s]=new(Pe++)etype(t, c,u,e[s]);
            e[t]=new(Pe++)etype(s,-c,0,e[t]);
            e[s]->pair=e[t];
            e[t]->pair=e[s];
        }
        do do memset(v,0,sizeof(v));
        while(aug(1,maxint));
        while(modlabel());
        printf("%d
    ",cost);
        return 0;
    }

    有兴趣深究的朋友可以从博文最后访问zkw大神的博客。

    其三:原始对偶算法

    讲这个算法之前先说一下zkw费用流的一些不适用性,zkw大神原话:在某一些图上, 算法速度非常快, 另一些图上却比纯 SPFA 增广的算法慢. 不少同学经过实测总结的结果是稠密图上比较快, 稀疏图上比较慢。其实也不完全是因为这样。我们比较一下原始的EK和zkw算法,可以发现原始EK主要慢在spfa一遍一遍的队列操作和重复访问节点,以及只能进行单路增广的限制。而zkw算法只是一个对边的扫描操作,并且重标号后可以多路增广。然而缺点也显而易见,如果这个图是出题者别有用心制造的,那么流量不大, 费用不小, 增广路还较长,每次添加一条边,然后尝试增广,凑不成最短路,再添重标号,继续尝试。造成了大量的时间浪费。

    那么有没有一种方式结合了这两种算法的优点呢?有的,我们可以用一种和dinic很相似的思路(至少笔者是这么认为)。

    费用流的算法大致分为两种, 一种是经典的解法, 如消圈, 增广路, 原始对偶等等, 特点是步步为营, 维持可行性或者最优性其中之一, 再不断对另一方面作出改进. 另一种就比较现代一些, 典型的例子是松弛算法和网络单纯形, 由于放松了对求解过程中解的限制条件, 使得其速度远远超过经典解法, 同时也增加了编程难度和理解障碍. 下面要说的原始对偶算法, 速度自然不可能比松弛和网络单纯形快, 但应该是经典解法中的佼佼者了。                                                                                       ——zkw

    我们在spfa的时候使用SLF优化来维护距离编号,然后利用多路增广,达到一个比较好的效果。下面放上笔者弱弱的代码:

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    #include<queue>
    #define ll long long
    #define inf 50000000
    #define re register
    using namespace std;
    struct po
    {
        int from,to,dis,nxt,w;
    }edge[250001];
    int head[250001],cur[1000001],dep[60001],n,m,s,t,u,num=-1,x,y,l,tot,sum,k,fa[10001];
    int dis[5001],b[5001],xb[5001],flow[5001];
    inline int read()
    {
        int x=0,c=1;
        char ch=' ';
        while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
        while(ch=='-')c*=-1,ch=getchar();
        while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
        return x*c;
    }
    inline void add_edge(int from,int to,int w,int dis)
    {
        edge[++num].nxt=head[from];
        edge[num].to=to;
        edge[num].w=w;
        edge[num].dis=dis;
        head[from]=num;
    }
    inline void add(int from,int to,int w,int dis)
    {
        add_edge(from,to,w,dis);
        add_edge(to,from,0,-dis);
    }
    inline bool spfa()
    {
        memset(b,0,sizeof(b));
        for(re int i=0;i<=n;i++) dis[i]=inf;
        dis[t]=0;b[t]=1;
        deque<int> q;
        q.push_back(t);
        while(!q.empty())
        {
            int u=q.front();
            b[u]=0;
            q.pop_front();
            for(re int i=head[u];i!=-1;i=edge[i].nxt)
            {
                int v=edge[i].to;
                if(edge[i^1].w>0&&dis[v]>dis[u]-edge[i].dis)
                {
                    dis[v]=dis[u]-edge[i].dis;
                    if(!b[v])
                    {
                        b[v]=1;
                        if(!q.empty()&&dis[v]<dis[q.front()])
                        q.push_front(v);
                        else
                        q.push_back(v);
                    }
                }
            }
        }
        return dis[s]<inf;
    }
    inline int dfs(int u,int low)
    {
        if(u==t)
        {
            b[t]=1;
            return low;
        }
        int diss=0;
        b[u]=1;
        for(re int i=head[u];i!=-1;i=edge[i].nxt)
        {
            int v=edge[i].to;
            if(!b[v]&&edge[i].w!=0&&dis[u]-edge[i].dis==dis[v])
            {
                int check=dfs(v,min(edge[i].w,low));
                if(check>0)
                {
                    tot+=check*edge[i].dis;
                    edge[i].w-=check;
                    edge[i^1].w+=check;
                    low-=check;
                    diss+=check;
                    if(low==0) break;
                }
            }
        }
        return diss;
    }
    inline int max_flow()
    {
        int ans=0;
        while(spfa())
        {
            b[t]=1;
            while(b[t])
            {
                memset(b,0,sizeof(b));
                ans+=dfs(s,inf);
            }
        }
        return ans;
    }
    int main()
    {
        memset(head,-1,sizeof(head));
        n=read();m=read();s=read();t=read();
        for(re int i=1;i<=m;i++)
        {
            x=read();y=read();l=read();int d=read();
            add(x,y,l,d);
        }
        cout<<max_flow()<<" ";
        cout<<tot;
    }
    View Code

      

    直接快了50%,可以看出多路增广的优势还是显然的。

    然而如果并没有看明白,可以访问以下这位大神的博客:一种更高效的费用流

    部分内容或有重复冲突请神犇们谅解,可能是想到一起去了。

    zkw大神的博客

  • 相关阅读:
    ASP.NET 后台弹出确认提示问题
    stretchableImageWithLeftCapWidth 自动适应UITableView
    UIBotton UIlabel Ios 下拉框
    cellForRowAtIndexPath UITableViewCell 选中后的背景颜色设置
    iOS 获得键盘的高度 NSNotificationCenter
    UIlabel 最小字体设置。
    NSMutableDictionary 与 NSMutableArray注意的地方
    iOS 背景图片。按钮高亮自定义背景
    iOS 判断当前输入法。UITextInputMode
    AudioServicesPlaySystemSound 系统声音提示 iOS iPad
  • 原文地址:https://www.cnblogs.com/victorique/p/8426361.html
Copyright © 2011-2022 走看看