1. 程式人生 > 實用技巧 ># 最小費用最大流

# 最小費用最大流

最小費用最大流

目錄

費用流就是在最大流的基礎上給每條邊加上了費用,求最大流的前提下使得總費用最小。

主要有兩種演算法:

  1. mcmf演算法—spfa單路增廣
  2. zkw費用流—spfa多路增廣,和dinic最大流演算法類似

兩種演算法實現步驟

  • mcmf

    1. 使用spfa求得源點到匯點的最短路,並記錄最短路徑上每個點的前驅
    2. 根據記錄的前驅,從匯點沿著前驅返回源點,對經過的邊減去這個最短路徑流過的流量,反向邊加上相應的流量
    3. 重複1、2,直到源點和匯點不連通
  • zkw

    1. 每次使用最短路演算法spfa,判斷源點和匯點是否連通,並且實現Dinic演算法中的分層效果(至於這裡為什麼要反向跑最短路,我也不是很清楚)
    2. 如果最短路得到源點和匯點連通,接下來進行多次dfs流量增廣,這裡和Dinic演算法基本一樣(dfs增廣和Dinic中的dfs增廣是一樣的)
    3. 重複1、2,直到源點和匯點不連通

    兩種演算法的直觀證明:每次最短路的結果都是從源點到匯點的最小費用,減去這條最小費用路徑上的流過的的流量,重複直到源點和匯點不連通,此時達到最大流量,由於每次費用都是當前圖中的最小費用,最後的費用自然就是最小費用。

效率分析

zkw部落格

實踐中,zkw效率非常奇怪,. 在某一些圖上, 演算法速度非常快, 另一些圖上卻比純 SPFA 增廣的演算法慢. 不少同學經過實測總結的結果是稠密圖上比較快, 稀疏圖上比較慢, 但也不盡然。

zkw從理論上分析得出:對於最終流量較大, 而費用取值範圍不大的圖, 或者是增廣路徑比較短的圖 (如二分圖), zkw 演算法都會比較快. 原因是充分發揮優勢. 比如流多說明可以同一費用反覆增廣, 費用窄說明不用改太多距離標號就會有新增廣路, 增廣路徑短可以顯著改善最壞情況, 因為即使每次就只增加一條邊也可以很快湊成最短路. 如果恰恰相反, 流量不大, 費用不小, 增廣路還較長, 就不適合 zkw 演算法了.

hzwer表示zkw不容易被卡,建議都使用zkw演算法。如果卡spfa或者你是“關於 SPFA,它死了”言論的追隨者,可以使用 Primal-Dual 原始對偶演算法將 SPFA 改成 Dijkstra!

程式碼模板

zkw演算法

洛谷3381 [模板] 最小費用最大流

給出點個數,有向邊個數,源點序號,匯點序號。給出邊的起點、終點、最大流量、單位流向的費用。

求最大流情況下的最小費用。

4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5

50 280

//鏈式前向星儲存
#include <cstdio>
#include <iostream>
#include <cstring>
#include <deque>
using namespace std;
const int N=5e3+5,M=1e5+5;
struct node{
    int from,to,flow,cost,next;
}e[M];
int h[N],idx;
int inf=0x3f3f3f3f;
bool vis[N];
//vis兩個用處:spfa裡的訪問標記,増廣時候的訪問標記
int dis[N];
//dis是spfa最短路的距離陣列
int n,m,s,t,ans;//ans是費用答案

inline void add(int from,int to,int flow,int cost){
    e[idx].from=from,e[idx].to=to,e[idx].flow=flow,e[idx].cost=cost,e[idx].next=h[from],h[from]=idx++;
}
inline void insert(int from,int to,int flow,int cost){
    add(from,to,flow,cost),add(to,from,0,-cost);
}
int q[N];
inline bool spfa(int s,int t){//反向跑最短路,求出距離標號
    memset(vis,0,sizeof(vis));//這裡的vis相當於inq,標記是否在佇列中
    memset(dis,0x3f,sizeof(dis));
    //首先SPFA我們維護距離標號的時候要倒著跑,這樣可以維護出到終點的最短路徑
    int hh=0,tt=0;
    q[tt++]=t;
    vis[t]=true,dis[t]=0;
    //使用了SPFA的SLF優化
    while(hh!=tt){
      // int now=p.front();p.pop_front();
        int now=q[hh++];if(hh==N+1)hh=0;

        for(int i=h[now],y;~i;i=e[i].next){
            if(e[i^1].flow>0){
                //首先c[k^1]是為什麼呢,因為我們要保證正流,但是SPFA是倒著跑的,所以說我們要求c[k]的對應反向邊是正的,這樣保證走的方向是正確的
                y=e[i].to;
                if(dis[now]-e[i].cost<dis[y]){
                    //因為已經是倒著的了,我們也可以很清楚明白地知道建邊的時候反向邊的邊權是負的,所以減一下就對了(負負得正)
                    dis[y]=dis[now]-e[i].cost;
                    if(!vis[y]){
                        vis[y]=true;
                        //slf優化
                        if(hh!=tt&&dis[y]<dis[now]){
                            --hh;
                            if(hh<0)hh=N;
                            q[hh]=y;
                        }
                        else {
                            q[tt++]=y;
                            if(tt==N+1)tt=0;
                        }
                    }
                }
            }
        }
        vis[now]=false;
    }
    
    //判斷起點和終點是否連通
    if(dis[s]==inf) return false;
    else return true;
}
inline int dfs(int x,int nowflow){//這裡是進行增廣
    if(x==t){vis[t]=true;return nowflow;}
    int totflow=0;vis[x]=true;
    //totflow表示從這個點總共可以增廣多少流量
    for(int i=h[x];~i;i=e[i].next){
        int y=e[i].to;
        if(!vis[y]&&e[i].flow>0&&dis[x]==dis[y]+e[i].cost){
            //這裡dis[x]==dis[y]+e[i].cost相當於確定x是由y更新過來的,作用相當於dinic演算法中的層次,表示x是y的下一層
            int canflow=dfs(y,min(e[i].flow,nowflow));
            //canflow表示從這條邊最多增廣的流量
            if(canflow){
                ans+=canflow*e[i].cost;//流量*單位費用
                e[i].flow-=canflow,e[i^1].flow+=canflow;
                totflow+=canflow;
                nowflow-=canflow;
            }
            if(nowflow<=0)break;
        }
    }
    return totflow;
}
inline int costFlow(){
    int flow=0;
    while(spfa(s,t)){//判斷起點終點是否連通,不連通說明滿流,做完了退出
        vis[t]=true;
       // cout<<cnt<<"--------"<<endl;
        while(vis[t]){
            memset(vis,0,sizeof(vis));
            flow+=dfs(s,inf);
        //    cout<<cnt<<endl;
        }
    }
    return flow;
}
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&t);
    memset(h,-1,sizeof(h));

    for(int i=0,from,to,flow,cost;i<m;i++){
        scanf("%d%d%d%d",&from,&to,&flow,&cost);
        insert(from,to,flow,cost);
    }
    int flow=costFlow();
    printf("%d %d",flow,ans);
   // cout<<ans<<endl;
    return 0;
}
/*
4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5

50 280
 */

mcmf演算法

洛谷 1251 餐巾計劃問題

題目描述 Description

一個餐廳在相繼的 N 天裡,每天需用的餐巾數不盡相同。假設第 i 天需要 ri塊餐巾(i=1,2,…,N)。餐廳可以購買新的餐巾,每塊餐巾的費用為 p 分;或者把舊餐巾送到快洗部,洗一塊需 m 天,其費用為 f 分;或者送到慢洗部,洗一塊需 n 天(n>m),其費用為 s<f 分。
每天結束時,餐廳必須決定將多少塊髒的餐巾送到快洗部,多少塊餐巾送到慢洗部,以及多少塊儲存起來延期送洗。但是每天洗好的餐巾和購買的新餐巾數之和,要滿足當天的需求量。
試設計一個演算法為餐廳合理地安排好 N 天中餐巾使用計劃,使總的花費最小。
程式設計找出一個最佳餐巾使用計劃.

輸入描述 Input Description

第 1 行有 6 個正整數 N,p,m,f,n,s。N 是要安排餐巾使用計劃的天數;p 是每塊新餐巾的費用;m 是快洗部洗一塊餐巾需用天數;f 是快洗部洗一塊餐巾需要的費用;n 是慢洗部洗一塊餐巾需用天數;s 是慢洗部洗一塊餐巾需要的費用。接下來的 N 行是餐廳在相繼的 N 天裡,每天需用的餐巾數。

輸出描述 Output Description

將餐廳在相繼的 N 天裡使用餐巾的最小總花費輸出

樣例輸入 Sample Input

3 10 2 3 3 2

5

6

7

樣例輸出 Sample Output

145

題解

這個問題的主要約束條件是每天的餐巾夠用,而餐巾的來源可能是最新購買,也可能是前幾天送洗,今天剛剛洗好的餐巾。每天用完的餐巾可以選擇送到快洗部或慢洗部,或者留到下一天再處理。

經過分析可以把每天要用的和用完的分離開處理,建模後就是二分圖。二分圖X集合中頂點Xi表示第i天用完的餐巾,其數量為ri,所以從S向Xi連線容量為ri的邊作為限制。Y集合中每個點Yi則是第i天需要的餐巾,數量為ri,與T連線的邊容量作為限制。每天用完的餐巾可以選擇留到下一天(Xi->Xi+1),不需要花費,送到快洗部(Xi->Yi+m),費用為f,送到慢洗部(Xi->Yi+n),費用為s。每天需要的餐巾除了剛剛洗好的餐巾,還可能是新購買的(S->Yi),費用為p。

#include<iostream>
#include<cstdio>
#define inf 0x7fffffff
#define T 2001
using namespace std;
int cnt=1,day,p,m,f,n,s,ans;
int from[2005],q[2005],dis[2005],head[2005];
bool inq[2005];
struct data{int from,to,next,v,c;}e[1000001];//v 流,c 費用
void ins(int u,int v,int w,int c)
{
    cnt++;
    e[cnt].from=u;e[cnt].to=v;
    e[cnt].v=w;e[cnt].c=c;
    e[cnt].next=head[u];head[u]=cnt;
}
void insert(int u,int v,int w,int c)
{ins(u,v,w,c);ins(v,u,0,-c);}
bool spfa()
{
    for(int i=0;i<=T;i++)dis[i]=inf;
    int t=0,w=1,i,now;
    dis[0]=q[0]=0;inq[0]=1;
    while(t!=w)//佇列,寬搜
    {
        now=q[t];t++;if(t==2001)t=0;
        for(int i=head[now];i;i=e[i].next)
        {
            if(e[i].v&&dis[e[i].to]>dis[now]+e[i].c)
            {
                from[e[i].to]=i;
                dis[e[i].to]=dis[now]+e[i].c;
                if(!inq[e[i].to])
                {
                    inq[e[i].to]=1;
                    q[w++]=e[i].to;
                    if(w==2001)w=0;
                }
            }
        }
        inq[now]=0;
    }
    if(dis[T]==inf)return 0;return 1;
}

void mcf()
{
    //x記錄這條最短路(源點到匯點的最小費用,求最短路過程中,點之間的邊權用費用代替)上的最小容量
    int i,x=inf;
    i=from[T];
    while(i)
    {
        x=min(e[i].v,x);
        i=from[e[i].from];
    }
    //x為這條增光路上的最小容量
    
    //根據記錄的前驅,從匯點沿著前驅返回源點,對經過的邊減去這個最短路徑流過的流量,反向邊加上相應的流量
    i=from[T];
    while(i)
    {
        e[i].v-=x;
        e[i^1].v+=x;
        ans+=x*e[i].c;
        i=from[e[i].from];
    }

}

int main()
{
    scanf("%d%d%d%d%d%d",&day,&p,&m,&f,&n,&s);
    for(int i=1;i<=day;i++)
    {
        if(i+1<=day)insert(i,i+1,inf,0);//起點,終點,容量,費用
        if(i+m<=day)insert(i,day+i+m,inf,f);
        if(i+n<=day)insert(i,day+i+n,inf,s);
        insert(0,day+i,inf,p);
    }
    int x;
    for(int i=1;i<=day;i++)
    {
        scanf("%d",&x);
        insert(0,i,x,0);
        insert(day+i,T,x,0);
    }
    while(spfa())mcf();
    printf("%d",ans);
    return 0;
}