1. 程式人生 > >圖論4——探索網絡流的足跡:Dinic算法

圖論4——探索網絡流的足跡:Dinic算法

sizeof 思想 png 概念 算法 如何 IT clu 百度

1. 網絡流:定義與簡析

1.1 網絡流是什麽?

網絡流是一種“類比水流的解決問題方法,與線性規劃密切相關”(語出百度百科)。

其實,在信息學競賽中,簡單的網絡流並不需要太高深的數學知識。

首先我們需要知道一些名詞是什麽意思:

  • 點(\(node\))。就是一個節點。點集通常用\(V\)表示。其中,有一個源點\(s\)和一個匯點\(t\),所有的流都從源點\(s\)出發,經過一些邊之後到達匯點\(t\)
  • 邊(\(edge\))。這個東西和大家在其他圖論知識中所用到的差不多,用於連接兩個點。邊集通常用\(E\)表示。圖\(G(V,E)\)表示由點集\(V\)和邊集\(E\)組成的圖\(G\)。在接下來講到的有關網絡流的問題中,邊都是有向的。
  • 容量(\(capacity\),簡稱\(cap\))。對於每一條邊,我們都有一個容量,表示這條邊最多能經過的流。

1.2 問題簡述

顯然,根據如上定義,到達匯點\(t\)的流是有限的。網絡流問題(NetWork Flow Problem)就是如何合理安排一種方式,使得從源點\(s\)到匯點\(t\)的流最多。

感覺枯燥嗎?其實我們可以這麽感性的理解。

你的家裏有\(10^{10^{10^{10}}}\)箱蘋果,然後你要通過一些高速公路把他們運到你的親戚家。由於某些限制,連接\(u\)\(v\)的高速公路\((u,v)\)一天只能允許載有不超過\(cap(u,v)\)的卡車通過。顯然,一天之內,能運送的蘋果數量是有限的,因為這些高速路的限制只能讓你運送其中的一些。那麽如何安排這些蘋果的運送方式,使得一天之內最後運到你親戚家的蘋果最多?

顯然,即使你把所有的\(10^{10^{10^{10}}}\)箱蘋果都運送給你的親戚,但是最後絕大多數都會在高速路收費站被攔下來——超載(並且最後被工作人員吃掉)。最多能夠送給親戚的蘋果數(即在最優方案下到達\(t\)的流)就稱之為最大流


2. 網絡流:嘗試與解決

2.1 貪心分析

A:這個問題不是可以貪心一遍然後\(O(n+m)\)就解決了嗎?

B:怎麽貪心?你做一遍試一下啊!

A:就是每一次都盡量多地往匯點送,如果下一條邊不能再運送這麽多的容量就分到其他條邊不就好了嗎?

B:(隨手一個圖)

技術分享圖片

A:然後我開始詳細講我的貪心算法了。

\(\quad\)首先我們從\(s\to 2\to 5\to t\)

運送\(3\)箱蘋果,從\(s\to 2\to 6\to t\)運送\(1\)箱蘋果;

\(\quad\)然後……\(s\to 3\to 5\to t\)就不行了……

\(\quad\)最後\(s\to 4\to 5\to t\)還有\(7\)箱。這樣合計\(11\)箱。(強行掩飾)那你有更好地解法嗎?

B:顯然有。如果你沒有這麽貪心,從\(s\to 2\to 5\to t\)只運送\(2\)箱蘋果,剩下兩箱從\(s\to 2\to 6\to t\)走,這樣\(s\to 3\to 5\to t\)還可以運一箱。最後\(s\to 4\to 5\to t\)還有\(7\)箱。合計\(2+2+1+7=12\)箱。

通過這段對話,我們發現,由於輸入順序未知,貪心很容易產生問題(盡管在這張圖中你有一半的可能得到正確答案,而在數據量大的時候,不出錯的概率很小)。所以我們考慮正確的解法。

2.2 樸素算法

在學習樸素算法之前,我們需要補充一個網絡流中常用到的定理。

如果設\(f(u,v)\)為實際從這條邊上走過的流量大小,則:

  1. 容量原則:\(f(u,v)\le cap(u,v)\).這個是顯然的,你不可能實際流量比限制容量還大(否則你會因為超載被警察叔叔開罰單)。
  2. 反對稱性:\(f(u,v)=-f(v,u)\).這個也是顯然的,你送給親戚\(10\)個蘋果不就相當於你從親戚那裏拿到了\(-10\)個蘋果嗎?(盡管聽上去總感覺好像不太正常)
  3. 流量守恒:當\(u\ne s,t\)時,如果規定邊\((u,v)\)\((v,u)\)都不存在時\(f(u,v)=f(v,u)=0\)的話,那麽\(\displaystyle\sum^{v\in V}_ {v} f(u,v) =0\)。這個並不是那麽好理解,簡單來說,因為從\(s\)點流出的流最終都匯入\(t\),所以中間所有邊都不會有剩下的流沒有出去。只有\(s\)\(t\)會“制造”和“接受”流量。(你不想讓高速路的工作人員把你的蘋果吃掉,所以一定選取剛好的蘋果給親戚;自然親戚會全部收到,所以高速路的工作人員沒有辦法攔下你的蘋果)。

有了這三條定理,我們就開始講樸素算法。

我們首先給每一條邊連上反向邊。這條反向邊的性質就像上面定理2“反對稱性”中的反向邊一樣。

由於目前的網絡流算法都基於增廣路思想,我們還是要介紹一下增廣路。

增廣路思想

增廣路定義:一條從源點到匯點的路徑,使得路徑上任意一條邊的殘量\(>0\)(註意是\(>\)而不是\(\ge\),這意味著這條邊還可以分配流量),這條路徑便稱為增廣路。

我們設\(g_{u,v}\)表示\((u,v)\)這條邊上的殘量,即剩余流量。

  1. 找到一條增廣路,記這條路徑上的邊集為\(P\)
  2. \(flow=\displaystyle\min_{(u,v)}^{(u,v)\in P} g_{u,v}\).
  3. 將這條路徑上的每一條有向邊\(u,v\)的殘量\(g_{u,v}\)減去\(flow\),同時對於反向邊\(v,u\)的殘量加上\(flow\)
  4. 重復上述過程,直到找不出增廣路,此時我們就找到了最大流。

為什麽反向邊要加上\(flow\)呢?因為對於下面這個例子:

技術分享圖片

所以需要加上\(flow\)

復雜度分析

這個復雜度是\(O(nm^2)\),而且往往不會少跑很多,所以只能用過\(n,m\le 300\)的數據。


Dinic算法

我們發現樸素算法有的時候會跑得特別特別的慢,因為增廣路徑查找得很不優秀。比如:

技術分享圖片

如果程序很不友好的在\((3,4)\)\((4,3)\)中來回跑,那麽顯然復雜度會高到離譜。所以在Dinic中,我們引入分層圖的概念。

分層圖

對於每一個點,我們根據從源點\(s\)開始的bfs序,為每一個點分配一個深度\(dep_i\),然後通過不斷dfs尋找增廣路,每一次dfs當且僅當\(dep_v=dep_u+1\)是才從\(u\)\(v\),這樣上面的情況就避免了。

復雜度估計

那麽Dinic的復雜度是多少呢?答案是\(O(n^2m)\)。事實上這只是最差情況下的估計,實際上遠沒有這麽大,對於\(n,m\le 10^5\)的數據往往是可以輕松過的。


代碼:

下面是代碼。其中\(d\)數組即\(dep_i\),每次通過bfs構造層次圖,然後dfs增廣。

#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;
const int MAXN=100010;
const int INF=0x3f3f3f3f;

    
class Dinic
{
    private:
        struct edge
        {
            int from,to,cap,flow;
        };
        vector<edge>e;  
        vector<int>f[MAXN]; 
        queue<int>q; 
        bool vis[MAXN];  
        int d[MAXN];  
        int cur[MAXN];   
        
        
        bool bfs()
        {
            memset(vis,0,sizeof(vis));
            while(!q.empty())
                q.pop();
            q.push(s);d[s]=0;vis[s]=1;
            while(!q.empty())
            {
                int x=q.front();q.pop();
                for(int i=0;i<f[x].size();i++)  
                {  
                    edge &y=e[f[x][i]];  
                    if(!vis[y.to]&&y.flow<y.cap)
                    {  
                        vis[y.to]=1;  
                        d[y.to]=d[x]+1; 
                        q.push(y.to);   
                    }  
                }  
            }
            return vis[t];
        }
        int dfs(int x,int a)
        {
            if(x==t)
                return a;  
            if(a==0)
                return 0;
            int flow=0,r;  
              
            for(int &i=cur[x];i<f[x].size();i++)  
            {  
                edge &y=e[f[x][i]];  
                if(d[x]+1==d[y.to]&&(r=dfs(y.to,min(a,y.cap-y.flow)))>0)  
                {  
                    y.flow+=r;  
                    e[f[x][i]^1].flow-=r;  
                    flow+=r;  
                    a-=r;  
                    if(a==0)
                        break;  
                }  
            }  
            return flow;  
        }
    
    public:
        int n,m,s,t;  
        void adde(int u,int v,int cap)
        {
            e.push_back((edge){u,v,cap,0});
            e.push_back((edge){v,u,0,0});
            this->m=e.size();
            f[u].push_back(m-2);
            f[v].push_back(m-1);
        }
        int maxflow()  
        {   
            int res=0;  
            while(bfs())  
            {
                memset(cur,0,sizeof(cur));  
                res+=dfs(s,INF);  
            }  
            return res;  
        }  
};

如果需要使用這個模板,只需要輸入\(n,s,t\),而\(m\)將根據加邊次數計算。

這個模板的技巧在於,存反向邊的時候利用了位運算\(xor\)的技巧:如果邊的編號從\(0\)計數,那麽相鄰的邊編號就是\(2x,2x+1\),在計算機中\(2x\ xor\ 1=2x+1\),而\((2x+1)\ xor\ 1=2x\)。這樣可以很方便的讀取反向邊。

圖論4——探索網絡流的足跡:Dinic算法