最小費用最大流模板


筆者在寫作這篇筆記之前做了整整兩天的最大流,然后。。。發現網絡流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\n",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大神的博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM