1. 程式人生 > >圖-網路流-最大流問題

圖-網路流-最大流問題

網路流是一種類比水流的解決問題的方法,與線性規劃密切相關。網路流的理論和應用在不斷髮展,具有增益的流、多終端流、多商品流以及網路流的分解與合成等新課題。網路流的應用已遍及通訊、運輸、電力、工程規劃、任務分派、裝置更新以及計算機輔助設計等眾多領域。

網路流是一個適用範圍相當廣的模型,相關的演算法也非常多。儘管如此,網路流中的概念、思想和基本演算法並不難理解。

首先看一下最大流問題。

對於一條邊(u,v),它的物品上限稱為容量(capacity),記為c(u,v)(對於不存在的邊(u,v),c(u,v)=0);實際運送的物品稱為流量(flow),記為f(u,v)。注意,把3個物品從u運送到v,又把5個物品從v運送到u沒什麼意義,因為它等價於把兩個物品從v運送到u。這樣,就可以規定f(u,v)和f(v,u)最多隻有一個正數(可以均為0),並且f(u,v)=-f(v,u)。這樣規定就好比把3個物品從u運送到v等價於把-3個物品從v運送到u一樣。

最大流問題的目標是把最多的物品從s運送到t,而其他結點都只是中轉,因此對於除了結點s和t外的任意結點u,f(u,v)之和=0(這些f中有些是負數)。從s運送出來的物品數目等於到達t的物品數目,而這正是此處最大化的目標。

在最大流問題中,容量c和流量f滿足三個性質:容量限制(f(u,v) <= c(u,v))、斜對稱性(f(u,v)= -f(v,u))和流量平衡(對於除了結點s和t外的任意結點u,\sum f(u,v)=0.問題的目標是最大化|f| = \sum f(s,v)=\sum f(u,t),即從s點流出的淨流量(它也等於流入點的淨流量)。

下面介紹求解最大流問題的演算法:增廣路演算法

演算法思想很簡單,從零流(所有邊的流量均為0)開始不斷增加流量,保持每次增加流量後都滿足容量限制、斜對稱性和流量平衡3個條件。計算出圖中的每條邊上容量與流量之差(稱為殘餘容量,簡稱殘量),得到殘量網路。

殘量網路中任何一條從s到t的有向道路都對應一條原圖中的增廣路-只要求出該道路中所有殘量的最小值d,把對應的所有邊上的流量增加d即可,這個過程稱為增廣。不難驗證,如果增廣前的流量滿足3個條件,增廣後仍然滿足。顯示,只要殘量網路存在增廣路,流量就可以增大。可以證明它的逆命題:如果殘量網路中不存在增廣路,則當前流就是最大流。這就是著名的增廣路定理。

找任意路徑最簡單的辦法無疑是用DFS,但很容易找出讓它很慢的例子,一個稍微好些的方法是使用BFS,這就是Edmonds-Karp演算法

邊的儲存結構定義:

struct Edge
{
    int from;
    int to;
    int flow;       //流量
    int cap;        //容量
    Edge(int u, int v, int f, int c):from(u), to(v), flow(f), cap(c){}
};

Edmonds-Karp演算法實現:

struct EdmondsKarp
{
    int n, m;
    vector<Edge> edges;     //邊數的兩倍
    vector<int> G[maxn];    //鄰接表,G[i][j]表示結點i的第j條邊在e陣列的序號
    int a[maxn];            //當起點到i的可改進量
    int p[maxn];            //最短路樹上的入弧編號

    void init(int n)
    {
        this->n = n;
        edges.clear();
        for(int i = 0; i < n; i++)
            G[i].clear();
    }

    void AddEdge(int from, int to, int cap)
    {
        edges.push_back(Edge(from, to, 0, cap));
        edges.push_back(Edge(to, from, 0, 0)); //反向弧
        m = edges.size();
        G[from].push_back(m-2);
        G[to].push_back(m-1);
    }

    int Maxflow(int s, int t)
    {
        int flow, x, i, u;
        flow = 0;
        for(;;)
        {
            memset(a, 0, sizeof(a));
            a[s] = INF;
            queue<int> Q;
            Q.push(s);

            while(!Q.empty())
            {
                x = Q.front();
                Q.pop();

                for(i = 0; i < G[x].size(); i++)
                {
                    Edge &e = edges[G[x][i]];
                    if(!a[e.to] && e.cap > e.flow)               //a[i]是正數,代替了原來的vis標誌陣列
                    {
                        a[e.to] = min(a[x], e.cap-e.flow);       //找小的殘量值
                        Q.push(e.to);
                        p[e.to] = G[x][i];
                    }
                }
                if(a[t])           //整條s-t道路上的最小殘量 到了t點退出
                    break;
            }
            if(!a[t])             //沒有增廣路,可以退出
                break;
            for(u = t; u != s; u = edges[p[u]].from)
            {
                edges[p[u]].flow += a[t];            //從t-s修改這條增廣路的流量
                edges[p[u]^1].flow -= a[t];          //修改反向弧的流量
            }
            flow += a[t];                            //累加
        }

        return flow;
    }
};

注意上面程式碼的一個技巧:每條弧和對應的反向弧儲存在一起。邊0和1互為反向邊,邊2和3互為反向邊。一般地,邊i的反向邊為i^1,其中^為二進位制異或運算子。

在拓展結點的同時還需遞推出從s到每個結點i的路徑上的最小殘量a[i][,則a[t]就是整條s-t道路上的最小殘量。另外,由於a[i]總是正數,所以用它代替了原來的vis標誌陣列。上面的程式碼把流初始化為零流,但這不是必需的。只要初始流是可行的,就可以用增廣路演算法進行增廣。

關於反向弧:在走反向弧的時候,相當於把正向弧的流量退了回去,退出的流量由另一條路接管,這樣便可以使程式有後悔的機會。