1. 程式人生 > >最大流(網路流基礎概念+三個演算法)

最大流(網路流基礎概念+三個演算法)

下面是由一道題引發的一系列故事。。。

Drainage Ditches
Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 68920 Accepted: 26683

Description

Every time it rains on Farmer John's fields, a pond forms over Bessie's favorite clover patch. This means that the clover is covered by water for awhile and takes quite a long time to regrow. Thus, Farmer John has built a set of drainage ditches so that Bessie's clover patch is never covered in water. Instead, the water is drained to a nearby stream. Being an ace engineer, Farmer John has also installed regulators at the beginning of each ditch, so he can control at what rate water flows into that ditch.
Farmer John knows not only how many gallons of water each ditch can transport per minute but also the exact layout of the ditches, which feed out of the pond and into each other and stream in a potentially complex network.
Given all this information, determine the maximum rate at which water can be transported out of the pond and into the stream. For any given ditch, water flows in only one direction, but there might be a way that water can flow in a circle.

Input

The input includes several cases. For each case, the first line contains two space-separated integers, N (0 <= N <= 200) and M (2 <= M <= 200). N is the number of ditches that Farmer John has dug. M is the number of intersections points for those ditches. Intersection 1 is the pond. Intersection point M is the stream. Each of the following N lines contains three integers, Si, Ei, and Ci. Si and Ei (1 <= Si, Ei <= M) designate the intersections between which this ditch flows. Water will flow through this ditch from Si to Ei. Ci (0 <= Ci <= 10,000,000) is the maximum rate at which water will flow through the ditch.

Output

For each case, output a single integer, the maximum rate at which water may emptied from the pond.

Sample Input

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

Sample Output

50

         直接看輸入輸出。輸入:n和m,分別代表邊的數量和節點數。之後的n行,輸入三個數,代表(u,v)邊的最大水容量。要求輸出,從起點1開始到終點m的最大水流量。那麼,與實際相聯絡,很容易知道,對於每一條路徑,都最大隻能運輸該路徑上的最小容量的那條邊所能承受的水量。所以,這個問題就是傳說中的網路流之最大流問題。

       經過查閱資料,先理清一下基本的概念:

容量網路:設G(V,E),是一個有向網路,在V中指定了一個頂點,稱為源點(記為Vs),以及另一個頂點,稱為匯點(記為Vt);對於每一條弧<u,v>屬於E,對應有一個權值c(u,v)>0,稱為弧的容量.通常吧這樣的有向網路G稱為容量網路.

弧的流量:通過容量網路G中每條弧<u,v>,上的實際流量(簡稱流量),記為f(u,v);

網路流:所有弧上流量的集合f={f(u,v)},稱為該容量網路的一個網路流.

可行流:在容量網路G中滿足以下條件的網路流f,稱為可行流.

    a.弧流量限制條件:   0<=f(u,v)<=c(u,v);

    b:平衡條件:即流入一個點的流量要等於流出這個點的流量,(源點和匯點除外).

若網路流上每條弧上的流量都為0,則該網路流稱為零流.

偽流:如果一個網路流只滿足弧流量限制條件,不滿足平衡條件,則這種網路流為偽流,或稱為容量可行流.(預流推進演算法有用)

最大流:在容量網路中,滿足弧流量限制條件,且滿足平衡條件並且具有最大流量的可行流,稱為網路最大流,簡稱最大流.

弧的型別:

a.飽和弧:即f(u,v)=c(u,v);

b.非飽和弧:即f(u,v)<c(u,v);

c.零流弧:即f(u,v)=0;

d.非零流弧:即f(u,v)>0.

鏈:在容量網路中,稱頂點序列(u1,u2,u3,u4,..,un,v)為一條鏈要求相鄰的兩個頂點之間有一條弧.

設P是G中一條從Vs到Vt的鏈,約定從Vs指向Vt的方向為正方向.在鏈中並不要求所有的弧的方向都與鏈的方向相同.

a.前向弧:(方向與鏈的正方向一致的弧),其集合記為P+,

b.後向弧:(方向與鏈的正方向相反的弧),其集合記為P-.

增廣路:

設f是一個容量網路G中的一個可行流,P是從Vs到Vt 的一條鏈,若P滿足以下條件:

a.P中所有前向弧都是非飽和弧,

b.P中所有後向弧都是非零弧.

則稱P為關於可行流f 的一條增廣路.

沿這增廣路改進可行流的操作稱為增廣.

殘留容量:給定容量網路G(V,E),及可行流f,弧<u,v>上的殘留容量記為cl(u,v)=c(u,v)-f(u,v).每條弧上的殘留容量表示這條弧上可以增加的流量.因為從頂點u到頂點v的流量減少,等效與從頂點v到頂點u的流量增加,所以每條弧<u,v>上還有一個反方向的殘留容量cl(v,u)=-f(u,v).

殘留網路:設有容量網路G(V,E)及其上的網路流f,G關於f的殘留網路記為G(V',E').其中G'的頂點集V'和G中頂點集G相同,V'=V.對於G中任何一條弧<u,v>,如果f(u,v)<c(u,v),那麼在G'中有一條弧<u,v>屬於E',其容量為c'(u,v)=c(u,v)-f(u,v),如果f(u,v)>0,則在G'中有一條弧<v,u>屬於E',其容量為c'(v,u)=f(u,v).殘留網路也稱為剩餘網路.


下面是所有最大流演算法的精華部分:引入反向邊

為什麼要有反向邊呢?

 

我們第一次找到了1-2-3-4這條增廣路,這條路上的delta值顯然是1。於是我們修改後得到了下面這個流。(圖中的數字是容量)

這時候(1,2)和(3,4)邊上的流量都等於容量了,我們再也找不到其他的增廣路了,當前的流量是1。

但這個答案明顯不是最大流,因為我們可以同時走1-2-4和1-3-4,這樣可以得到流量為2的流。

那麼我們剛剛的演算法問題在哪裡呢?問題就在於我們沒有給程式一個”後悔”的機會,應該有一個不走(2-3-4)而改走(2-4)的機制。那麼如何解決這個問題呢?回溯搜尋嗎?那麼我們的效率就上升到指數級了。

而這個演算法神奇的利用了一個叫做反向邊的概念來解決這個問題。即每條邊(I,j)都有一條反向邊(j,i),反向邊也同樣有它的容量。

我們直接來看它是如何解決的:

在第一次找到增廣路之後,在把路上每一段的容量減少delta的同時,也把每一段上的反方向的容量增加delta。即在Dec(c[x,y],delta)的同時,inc(c[y,x],delta)

我們來看剛才的例子,在找到1-2-3-4這條增廣路之後,把容量修改成如下

這時再找增廣路的時候,就會找到1-3-2-4這條可增廣量,即delta值為1的可增廣路。將這條路增廣之後,得到了最大流2。

那麼,這麼做為什麼會是對的呢?我來通俗的解釋一下吧。

事實上,當我們第二次的增廣路走3-2這條反向邊的時候,就相當於把2-3這條正向邊已經是用了的流量給”退”了回去,不走2-3這條路,而改走從2點出發的其他的路也就是2-4。(有人問如果這裡沒有2-4怎麼辦,這時假如沒有2-4這條路的話,最終這條增廣路也不會存在,因為他根本不能走到匯點)同時本來在3-4上的流量由1-3-4這條路來”接管”。而最終2-3這條路正向流量1,反向流量1,等於沒有流量。

這就是這個演算法的精華部分,利用反向邊,使程式有了一個後悔和改正的機會

第一個隆重登場的演算法是 EK(Edmond—Karp)演算法

先給出模板(也是為了方便以後自己查閱)

<strong><span style="font-size:12px;">#include <iostream>
#include <queue>
#include<string.h>
using namespace std;
#define arraysize 201
int maxData = 0x7fffffff;
int capacity[arraysize][arraysize]; //記錄殘留網路的容量
int flow[arraysize];                //標記從源點到當前節點實際還剩多少流量可用
int pre[arraysize];                 //標記在這條路徑上當前節點的前驅,同時標記該節點是否在佇列中
int n,m;
queue<int> myqueue;
int BFS(int src,int des)
{
    int i,j;
    while(!myqueue.empty())       //佇列清空
        myqueue.pop();
    for(i=1;i<m+1;++i)
    {
        pre[i]=-1;
    }
    pre[src]=0;
    flow[src]= maxData;
    myqueue.push(src);
    while(!myqueue.empty())
    {
        int index = myqueue.front();
        myqueue.pop();
        if(index == des)            //找到了增廣路徑
            break;
        for(i=1;i<m+1;++i)
        {
            if(i!=src && capacity[index][i]>0 && pre[i]==-1)
            {
                 pre[i] = index; //記錄前驅
                 flow[i] = min(capacity[index][i],flow[index]);   //關鍵:迭代的找到增量
                 myqueue.push(i);
            }
        }
    }
    if(pre[des]==-1)      //殘留圖中不再存在增廣路徑
        return -1;
    else
        return flow[des];
}
int maxFlow(int src,int des)
{
    int increasement= 0;
    int sumflow = 0;
    while((increasement=BFS(src,des))!=-1)
    {
         int k = des;          //利用前驅尋找路徑
         while(k!=src)
         {
              int last = pre[k];
              capacity[last][k] -= increasement; //改變正向邊的容量
              capacity[k][last] += increasement; //改變反向邊的容量
              k = last;
         }
         sumflow += increasement;
    }
    return sumflow;
}
int main()
{
    int i,j;
    int start,end,ci;
    while(cin>>n>>m)
    {
        memset(capacity,0,sizeof(capacity));
        memset(flow,0,sizeof(flow));
        for(i=0;i<n;++i)
        {
            cin>>start>>end>>ci;
            if(start == end)               //考慮起點終點相同的情況
               continue;
            capacity[start][end] +=ci;     //此處注意可能出現多條同一起點終點的情況
        }
        cout<<maxFlow(1,m)<<endl;
    }
    return 0;
}</span></strong>


EK演算法的核心
反覆尋找源點s到匯點t之間的增廣路徑,若有,找出增廣路徑上每一段[容量-流量]的最小值delta,若無,則結束。
在尋找增廣路徑時,可以用BFS來找,並且更新殘留網路的值(涉及到反向邊)。
而找到delta後,則使最大流值加上delta,更新為當前的最大流值。

對於BFS找增廣路:

1.         flow[1]=INF,pre[1]=0;

        源點1進佇列,開始找增廣路,capacity[1][2]=40>0,則flow[2]=min(flow[1],40)=40;

        capacity[1][4]=20>0,則flow[4]=min(flow[1],20)=20;

        capacity[2][3]=30>0,則flow[3]=min(folw[2]=40,30)=30;

        capacity[2][4]=30,但是pre[4]=1(已經在capacity[1][4]這遍歷過4號點了)

        capacity[3][4].....

        當index=4(匯點),結束增廣路的尋找

        傳遞迴increasement(該路徑的流),利用前驅pre尋找路徑

路徑也自然變成了這樣:

2.flow[1]=INF,pre[1]=0;

 源點1進佇列,開始找增廣路,capacity[1][2]=40>0,則flow[2]=min(flow[1],40)=40;

        capacity[1][4]=0!>0,跳過

        capacity[2][3]=30>0,則flow[3]=min(folw[2]=40,30)=30;

        capacity[2][4]=30,pre[4]=2,則flow[2][4]=min(flow[2]=40,20)=20;

        capacity[3][4].....

        當index=4(匯點),結束增廣路的尋找

        傳遞迴increasement(該路徑的流),利用前驅pre尋找路徑

 圖也被改成

  

接下來同理

這就是最終完成的圖,最終sumflow=20+20+10=50(這個就是最大流的值)

下面是我用這個方法寫的本題的程式碼

<strong><span style="font-size:12px;">#include <cstdio>
#include <algorithm>
#include <queue>
#include <string.h>
using namespace std;
int const MAX = 1005;
int const inf = 0x3f3f3f3f;
int c[MAX][MAX];//c[u][v]儲存容量
int f[MAX][MAX];//f[u][v]儲存當前流量
int a[MAX];// a陣列在每趟bfs中找到最小路徑中最小殘餘流量的,a陣列使個遞推陣列,a[v]的意思是從源點s到點v的最小殘餘流量
int p[MAX];//儲存前一個點
int n, m;
int bfs(int s, int t)
{
    queue<int> q;
    int flow = 0;
    while(!q.empty())   q.pop();
    memset(f, 0, sizeof(f));
    while(1){
        memset(a, 0, sizeof(a));
        a[s] = inf;//將起始點的最小殘餘量設為最大
        q.push(s);
        while(!q.empty()){//bfs找到一條最短路,這裡的邊不代表距離,可以看作每兩個點都是單位距離的
            int u;
            u = q.front();
            q.pop();
            for(int v = 1; v <= m; v++){//列舉所有點v <u,v>
                if(!a[v] && c[u][v] > f[u][v]){//a[]可以代替vis[],來判斷這個點是否已經遍歷過,後面那個條件更是起了關鍵作用,很巧妙
                    p[v] = u;
                    q.push(v);
                    a[v] = min(a[u], c[u][v] - f[u][v]);//遞推
                }
            }
        }
        if(!a[t])   break;//直到最小殘餘流量為0時,退出
        for(int u = t; u != s; u = p[u]){
            f[p[u]][u] += a[t];
            f[u][p[u]] -= a[t];
        }
        flow += a[t];
    }
    return flow;
}

int main()
{
    while(~scanf("%d %d", &n, &m)){
        memset(c, 0, sizeof(c));
        memset(p, 0, sizeof(p));
        for(int i = 1; i <= n; i++){
            int u, v, w;
            scanf("%d %d %d", &u, &v, &w);
            c[u][v] += w;
        }
        printf("%d\n", bfs(1, m));
    }
    return 0;
}</span></strong>

第二個隆重登場的演算法,Ford-Fulkerson演算法,簡單易懂,老少皆宜

基於鄰接矩陣的一個模板

<strong><span style="font-size:12px;">#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
int map[300][300];
int used[300];
int n,m;
const int INF = 1000000000;
int dfs(int s,int t,int f)
{
    if(s == t) return f;
    for(int i = 1 ; i <= n ; i ++) {
        if(map[s][i] > 0 && !used[i]) {
            used[i] = true;
            int d = dfs(i,t,min(f,map[s][i]));
            if(d > 0) {
                map[s][i] -= d;
                map[i][s] += d;
                return d;
            }
        }
    }
}
int maxflow(int s,int t)
{
    int flow = 0;
    while(true) {
        memset(used,0,sizeof(used));
        int f = dfs(s,t,INF);//不斷找從s到t的增廣路
        if(f == 0) return flow;//找不到了就回去
        flow += f;//找到一個流量f的路
    }
}
int main()
{
    while(scanf("%d%d",&m,&n) != EOF) {
        memset(map,0,sizeof(map));
        for(int i = 0 ; i < m ; i ++) {
            int from,to,cap;
            scanf("%d%d%d",&from,&to,&cap);
            map[from][to] += cap;
        }
        cout << maxflow(1,n) << endl;
    }
    return 0;</span>
}</strong>

下面是我用vector寫的Ford-Fulkerson演算法的本題程式碼
<span style="font-size:12px;"><strong>#include <cstdio>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;
int const inf = 0x3f3f3f3f;
int const MAX = 300;
struct Node
{
    int to;  //與這個點相連的點
    int cap; //以這個射出的邊的容量
    int rev; //這個點的反向邊
};
vector<Node> v[MAX];
bool used[MAX];

void add_node(int from, int to, int cap)//重邊情況不影響
{
    v[from].push_back((Node){to, cap, v[to].size()});
    v[to].push_back((Node){from, 0, v[from].size() - 1});
}
int dfs(int s, int t, int f)
{
    if(s == t)
        return f;
    used[s] = true;
    for(int i = 0; i < v[s].size(); i++){
        Node &tmp = v[s][i];
        if(used[tmp.to] == false && tmp.cap > 0){
            int d = dfs(tmp.to, t, min(f, tmp.cap));
            if(d > 0){
                tmp.cap -= d;
                v[tmp.to][tmp.rev].cap += d;
                return d;
            }
        }
    }
    return 0;
}
int max_flow(int s, int t)
{
    int flow = 0;
    while(1){
        memset(used, false, sizeof(used));
        int f = dfs(s, t, inf);
        if(f == 0)
            return flow;
        flow += f;
    }
    return flow;
}
int main()
{
    int n, m;
    while(scanf("%d %d", &n, &m) != EOF){
        for(int i = 0; i <= m; i++)
            v[i].clear();
        int u1, v1, w;
        for(int i = 1; i <= n; i++){
            scanf("%d %d %d", &u1, &v1, &w);
            add_node(u1, v1, w);
        }
        printf("%d\n", max_flow(1, m));
    }
    return 0;
}</strong></span>

第三種方法:Dinic演算法,可以看作是兩種方法的結合體,它進行了一定的優化,對於某些橫邊多的圖,執行速度方面得到了大幅提升

Dinic演算法的基本思路:
       根據殘量網路計算層次圖。

       在層次圖中使用DFS進行增廣直到不存在增廣路

       重複以上步驟直到無法增廣

  • 層次圖:分層圖,以[從原點到某點的最短距離]分層的圖,距離相等的為一層,(比如上圖的分層為{1},{2,4},{3})
       觀察前面的dfs演算法,對於層次相同的邊,會經過多次重複運算,很浪費時間,那麼,可以考慮先對原圖分好層產生新的層次圖,即儲存了每個點的層次,注意,很多人會把這裡的邊的最大容量跟以前算最短路時的那個權值混淆,其實這裡每個點之間的距離都可以看作單位距離,然後對新圖進行dfs,這時的dfs就非常有層次感,有篩選感了,同層次的點不可能在同一跳路徑中,直接排除。那麼執行速度就會快很多了。
<span style="font-size:12px;"><strong>#include <cstdio>
#include <string.h>
#include <queue>
using namespace std;
int const inf = 0x3f3f3f3f;
int const MAX = 205;
int n, m;
int c[MAX][MAX], dep[MAX];//dep[MAX]代表當前層數

int bfs(int s, int t)//重新建圖,按層次建圖
{
    queue<int> q;
    while(!q.empty())
        q.pop();
    memset(dep, -1, sizeof(dep));
    dep[s] = 0;
    q.push(s);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int v = 1; v <= m; v++){
            if(c[u][v] > 0 && dep[v] == -1){//如果可以到達且還沒有訪問,可以到達的條件是剩餘容量大於0,沒有訪問的條件是當前層數還未知
                dep[v] = dep[u] + 1;
                q.push(v);
            }
        }
    }
    return dep[t] != -1;
}

int dfs(int u, int mi, int t)//查詢路徑上的最小流量
{
    if(u == t)
        return mi;
    int tmp;
    for(int v = 1; v <= m; v++){
        if(c[u][v] > 0 && dep[v] == dep[u] + 1  && (tmp = dfs(v, min(mi, c[u][v]), t))){
            c[u][v] -= tmp;
            c[v][u] += tmp;
            return tmp;
        }
    }
    return 0;
}

int dinic()
{
    int ans = 0, tmp;
    while(bfs(1, m)){
        while(1){
            tmp = dfs(1, inf, m);
            if(tmp == 0)
                break;
            ans += tmp;
        }
    }
    return ans;
}

int main()
{
    while(~scanf("%d %d", &n, &m)){
        memset(c, 0, sizeof(c));
        int u, v, w;
        while(n--){
            scanf("%d %d %d", &u, &v, &w);
            c[u][v] += w;
        }
        printf("%d\n", dinic());
    }
    return 0;
}</strong></span>

洗洗睡了。。明天繼續研究其他幾個網路流的演算法