1. 程式人生 > >最大流模板(一)BFS

最大流模板(一)BFS

Drainage Ditches
參考部落格
用這篇部落格學習了一下bfs求解最大流,然後先做一下筆記。(以下均為筆者自己理解,有不對之處還望大佬指出)
首先,我們學習最大流就要先了解什麼是最大流。

最大流問題定義:管道網路中每條邊的最大通過能力(容量)是有限的,實際流量不超過容量。最大流問題(maximum flow problem),一種組合最優化問題,就是要討論如何充分利用裝置的能力,使得運輸的流量最大,以取得最好的效果。(摘自百度百科)
就是,我們給定一個圖,這個圖一般是一個有向圖(下面討論的都是有向圖),其中的方向就是流向。其中還有一個源點和匯點。源點是圖中不被任何節點指向的那個點,也就是可以想成這個源點向別的節點輸出流,這個流的流量可以是無限大。匯點是不指向任何其他節點的點,由源點流出的最終彙集在這個節點。
那麼一個圖中由源點到匯點就會有很多條路徑。我們要選擇其中的有限條路徑達到可以使得從源點到匯點的流量最大。
每兩個節點之間有一條邊(可以看做是一條通道,或是河流),因為每一段河流或說通道都有自己的運載能力(或說是容量),不可能通過無限的流,所以圖中每一條邊都有一個權值,這個權值代表的是這一條邊的最大容量(即能通過的最大流量)。
最大流問題就是讓我們選擇網路流的路線,以達到從源點到匯點能通過的流(流量)最大。
這樣就用到了我們對圖的遍歷演算法——bfs。我們對一個圖進行bfs遍歷的時候,大多數情況是找到一條路徑就退出了,所以我們需要一直bfs找路徑,找到所有的可行路徑,讓所有路徑並行(就是每一次bfs求得的最大流量加和)以使得整個圖流量最大。
這裡,我們找到路徑以後,就在經過的邊上減去這部分流量,從剩下的流量裡繼續找路徑。可是有一個問題,我們這樣找可能找的並不恰當。
因為可能原本有兩條路徑,但是在遍歷過程中,其中一條路被另一條擠掉了,但這條路走其他路徑也完全可以到達匯點(這是因為我們的遍歷並不是智慧的,畢竟計算機不會分析這樣會不會擠掉另一條路),但這個時候我們這是後已經減掉了這部分流量。那麼我們就反向加回來,當做是一種補償,也可以是想成添加了反向流與一部分原來流抵消了。比如我們找到一條路徑,那麼會graph[u][v] -= min_flow;反向流就是加一個反向的同樣大的流:graph[v][u] += min_flow;其實就是加了條反向邊。

下面開始說一下這個反向流(這是我自己亂說的名詞)的作用:
我理解的反向流的本質就是抵消錯誤流,等我解釋完可能讀者就能明白了。
首先,我們先假設,從源點到匯點只有兩條相互獨立(兩條路徑不相交)的路徑(注意不是邊)(用於解釋這個反向流),

  • 如果這兩條路徑中間沒有其他邊連線的話,那麼這樣bfs就不會發生衝突了,所以就不用考慮了,直接不停bfs就行了。當然也就不用考慮反向流。
  • 如果這兩條路徑之間被一條邊連線了(假設是從第一條路徑上除源點和匯點的節點指向第二條路徑中的某個點,讀者可以自己在紙上畫個草圖,便於理解)。那麼當我們在bfs過程中就有可能發生這樣的情況:第一條路徑同過這條邊走到了第二條路徑並且到達了匯點。那麼問題來了,第二條路徑的後半段被佔用了,那麼在遍歷第二條路徑的時候,我們走到與這個邊的交點時,我們就走不下去了(如果沒有反向流),因為後面被佔了,聯通兩條路徑的邊也被佔用了,那麼這時候我們的程式就會認為已經找不到別的路徑了,返回回來這個流量,那麼顯然是不對的。因為明明可以是兩條路徑的,卻被擠佔掉了。那麼怎麼辦呢,這時候反向流就發揮作用了。我們在遍歷第二條路徑的時候走到交點,發現後面的那一段路不能走了,但是兩條路徑中間剛剛還增加了一條反向流,第二條路徑便會沿著這個反向流走到第一條路徑的後半段。發現,這條路後面還有容量可以承載流,那麼就從這裡走了。這樣,雖然兩條路徑連通了,並且發生了交叉,但是並沒有影響流的傳輸,不會存在被擠佔路徑的問題。和兩條獨立路徑分別走是一樣的效果。這樣就將剛剛走錯的流抵消掉了,相當於兩條路徑各走各的互不干擾。

(說到這裡就差不多了,筆者語言敘述能力不強,又怕描述不清,所以就寫了這麼冗長的一段,如果還是沒講清楚,可以結合程式碼,可能就明白了,請見諒。)
下面就是程式碼實現了:

#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn = 2e2 + 5, inf = 0x3f3f3f3f;
int graph[maxn][maxn];

int max_flow(int num, int source, int sink) //num代表圖中節點個數,source代表源點,sink代表匯點
{
    int pre[maxn], min_flow[maxn];  //pre記錄節點的父節點,min_flow記錄某節點最多可以經過的流
    int ans = 0;
    while(true) //一直bfs直到找不到增廣路徑
    {
        queue<int> q;
        memset(pre, -1, sizeof(pre));   //父節點全都設為不存在
        pre[0] = -2;    min_flow[0] = inf;  //源點的父節點單獨處理,假設我們從源頭開始可以流出無限多
        q.push(source);
        while(q.size()) //bfs尋找增廣路徑
        {
            int temp = q.front();
            q.pop();

            for(int i = 1; i < num; i++)
            {
                if(!graph[temp][i] || pre[i] != -1) continue ;  //如果這個節點已經走過或者已經容量為0了,那麼就跳過
                q.push(i);  //將可行子節點入隊
                pre[i] = temp;  //記錄i的父節點
                //比較父節點和子節點之間的剩餘能通過的流和父節點能通過的流量,求最小,因為有一個地方通道小了,那麼流量就被限制了
                min_flow[i] = min(graph[temp][i], min_flow[temp]);
            }
            //如果匯點有了父節點說明已經找到了一條路徑,更改圖中的流量返回,開始找一條新的路徑
            if(pre[sink] != -1)
            {
                int son = sink;
                while(pre[son] >= 0)    //一直往前找並且更節點之間的最大通量
                {
                    //因為找到了一條路徑,就需要在整個圖的通量中減去這一條路徑,
                    //或者理解成為這條路徑預留出來這麼多通量,然後從剩下的裡面繼續找路徑
                    graph[pre[son]][son] -= min_flow[sink];
                    //這裡給他一個後悔的機會,正向走一次,通量消耗了那麼多,相當於加在了反向通量上
                    graph[son][pre[son]] += min_flow[sink];
                    son = pre[son];
                }
                break ;
            }//end of finding source
        }//end of bfs
        if(pre[sink] != -1) //說明bfs還能找到增廣路徑,那麼將這條路徑加入總答案,繼續找
            ans += min_flow[sink];
        else    //否則說明找不到增廣路徑了,那麼說明當前已經是最終答案了,返回
            return ans;
    }
}

int main()
{
    int m, n;    //n流通數,m為節點數量
    while(~scanf("%d %d", &n, &m))
    {
        int a, b, cost;
        memset(graph, 0, sizeof(graph));
        for(int i = 0; i < n; i++)
        {
			scanf("%d%d%d", &a, &b, &cost);
            graph[a-1][b-1] += cost;//網路
        }
        printf("%d\n", max_flow(m, 0, m-1));
    }
    return 0;
}