《數據結構與算法分析:C語言描述》複習——第九章“圖論”——最大流問題(每次尋找增廣路徑.......)與Dinic算法

參考網站:https://www.cnblogs.com/zhuli19901106/p/3825152.html

https://www.cnblogs.com/SYCstudio/p/7260613.html

 

每次找到一條增廣路徑, 就給這條路徑增流 , 增加的流量就可以計入到最大流內了, 因爲不可能從匯點T有迴流,所以只要增流的都可以計入最大流. (流網絡必須滿足有源點和匯點,否則算法應該跑的結果是沒有意義的).——這就是增廣路徑的想法。

 

因此求出最大流量後,在這樣的流量模型裏,s作爲入口,t作爲出口,其他頂點的流入和流出一定會保持平衡。這僅僅是最簡單的一種模型,如果允許多個入口或者多個出口的話,那麼問題的複雜程度就不是幾句話能說清楚的了。

 

Ford-Fulkerson方法是基於這種增廣路徑思想的解法,用於處理這類問題。之所以叫“方法”而不是“算法”,是因爲如何尋找增廣路徑並沒有具體說明。

  那到底怎麼找呢?可以用BFS、DFS、排序等等手段來組合使用。簡單的來說,就是靠搜。根據尋找增廣路徑的方法不同,就有了幾種不同的對應算法。

 

例:若使用BFS來尋找增廣路徑,則屬於Edmonds-Karp算法。

 

爲什麼添加了反向邊?

可以這樣想:一條邊現在從u到v的流量是x。那麼加上一條增廣路之後可能使得這條邊的流量變小,小於x,但總的從原點到匯點流量增加了。但是這條邊流量最小變爲0,因爲這條邊流向只能是u到v。所以增加的這條增廣路中,在這條邊上的流量最大可以是從v到u,流量爲x,也就是現在從u到v的流量。一條增廣路可以把這條邊的流量抵消爲0,但不能抵消爲負數,因爲不能改變網絡的流向。所以要加上反向邊,反向邊的流量最大爲x,也就是這條邊最大被抵消的能力。

 

爲什麼要連反向邊?

我們知道,當我們在尋找增廣路的時候,在前面找出的不一定是最優解,如果我們在減去殘量網絡中正向邊的同時將相對應的反向邊加上對應的值,我們就相當於可以反悔從這條邊流過。
比如說我們現在選擇從 u 流向 v 一些流量,但是我們後面發現,如果有另外的流量從 p 流向 v,而原來 u 流過來的流量可以從 u->q 流走,這樣就可以增加總流量,其效果就相當於 p->v->u->q,用圖表示就是:
此處輸入圖片的描述
圖中的藍色邊就是我們首次增廣時選擇的流量方案,而實際上如果是橘色邊的話情況會更優,那麼我們可以在 v->u 之間連一條邊容量爲 u->v 減去的容量,那我們在增廣 p->v->u->q 的時候就相當於走了 v->u 這條 "邊",而 u->v 的流量就與 v->u 的流量相抵消,就成了中間那幅圖的樣子了。
如果是 v->u 時的流量不能完全抵消 u->v 的,那就說明 u 還可以流一部分流量到 v,再從 v 流出,這樣也是允許的。

 

一個小技巧

雖然說我們已經想明白了爲什麼要加反向邊,但反向邊如何具體實現呢?筆者在學習網絡流的時候在這裏困擾了好久,現在簡要的總結在這裏。
首先講一下鄰接矩陣的做法,對於 G[u][v],如果我們要對其反向邊進行處理,直接修改 G[v][u] 即可。
但有時會出現 u->v 和 v->u 同時本來就有邊的情況,一種方法是加入一個新點 p,使 u->v,而 v->u 變成 v->p,p->u。
另一種方法就是使用鄰接表,我們把邊從 0 開始編號,每加入一條原圖中的邊 u->v 時,加入邊 v->u 流量設爲 0,那麼這時對於編號爲 i 的邊 u->v,我們就可以知道 i^1 就是其反向邊 v->u。

 

樸素算法的低效之處

雖然說我們已經知道了增廣路的實現,但是單純地這樣選擇可能會陷入不好的境地,比如說這個經典的例子:
此處輸入圖片的描述
我們一眼可以看出最大流是 999(s->v->t)+999(s->u->t),但如果程序採取了不恰當的增廣策略:s->v->u->t
此處輸入圖片的描述
我們發現中間會加一條 u->v 的邊
此處輸入圖片的描述
而下一次增廣時:
此處輸入圖片的描述
若選擇了 s->u->v->t
此處輸入圖片的描述
然後就變成
此處輸入圖片的描述
這是個非常低效的過程,並且當圖中的 999 變成更大的數時,這個劣勢還會更加明顯。
怎麼辦呢?
這時我們引入 Dinic 算法

Dinic 算法

爲了解決我們上面遇到的低效方法,Dinic 算法引入了一個叫做分層圖的概念。具體就是對於每一個點,我們根據從源點開始的 bfs 序列,爲每一個點分配一個深度,然後我們進行若干遍 dfs 尋找增廣路,每一次由 u 推出 v 必須保證 v 的深度必須是 u 的深度 + 1。下面給出代碼
一些變量的定義

int s,t;//源點和匯點
int cnt;//邊的數量,從0開始編號。
int Head[maxN];//每一個點最後一條邊的編號
int Next[maxM];//指向對應點的前一條邊
int V[maxM];//每一條邊指向的點
int W[maxM];//每一條邊的殘量
int Depth[maxN];//分層圖中標記深度

Dinic 主過程:

int Dinic()
{
    int Ans=0;//記錄最大流量
    while (bfs())
    {
        while (int d=dfs(s,inf))
            Ans+=d;
    }
    return Ans;
}

bfs 分層圖過程

bool bfs()
{
    queue<int> Q;//定義一個bfs尋找分層圖時的隊列
    while (!Q.empty())
        Q.pop();
    memset(Depth,0,sizeof(Depth));
    Depth[s]=1;//源點深度爲1
    Q.push(s);
    do
    {
        int u=Q.front();
        Q.pop();
        for (int i=Head[u];i!=-1;i=Next[i])
            if ((W[i]>0)&&(Depth[V[i]]==0))//若該殘量不爲0,且V[i]還未分配深度,則給其分配深度並放入隊列
            {
                Depth[V[i]]=Depth[u]+1;
                Q.push(V[i]);
            }
    }
    while (!Q.empty());
    if (Depth[t]==0)//當匯點的深度不存在時,說明不存在分層圖,同時也說明不存在增廣路
        return 0;
    return 1;
}

dfs 尋找增廣路過程

int dfs(int u,int dist)//u是當前節點,dist是當前流量
{
    if (u==t)//當已經到達匯點,直接返回
        return dist;
    for (int i=Head[u];i!=-1;i=Next[i])
    {
        if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))//注意這裏要滿足分層圖和殘量不爲0兩個條件
        {
            int di=dfs(V[i],min(dist,W[i]));//向下增廣
            if (di>0)//若增廣成功
            {
                W[i]-=di;//正向邊減
                W[i^1]+=di;反向邊加
                return di;//向上傳遞
            }
        }
    }
    return 0;//否則說明沒有增廣路,返回0
}

把上面的內容都封裝到類中:

class Graph
{
private:
    int s,t;
    int cnt;
    int Head[maxN];
    int Next[maxM];
    int V[maxM];
    int W[maxM];
    int Depth[maxN];
public:
    int n;
    void init(int nn,int ss,int tt)//初始化
        {
            n=nn;
            s=ss;
            t=tt;
            cnt=-1;
            memset(Head,-1,sizeof(Head));
            memset(Next,-1,sizeof(Next));
            return;
        }
    void _Add(int u,int v,int w)
        {
            cnt++;
            Next[cnt]=Head[u];
            V[cnt]=v;
            W[cnt]=w;
            Head[u]=cnt;
        }
    void Add_Edge(int u,int v,int w)//加邊,同時加正向和反向的
        {
            _Add(u,v,w);
            _Add(v,u,0);
        }
    int dfs(int u,int dist)
        {
            //cout<<"Dfs:"<<u<<' '<<dist<<endl;
            if (u==t)
                return dist;
            for (int i=Head[u];i!=-1;i=Next[i])
            {
                if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))
                {
                    int di=dfs(V[i],min(dist,W[i]));
                    if (di>0)
                    {
                        W[i]-=di;
                        W[i^1]+=di;
                        return di;
                    }
                }
            }
            return 0;
        }
    int bfs()
        {
            //cout<<"Bfs.begin:"<<endl;
            queue<int> Q;
            while (!Q.empty())
                Q.pop();
            memset(Depth,0,sizeof(Depth));
            Depth[s]=1;
            Q.push(s);
            do
            {
                int u=Q.front();
                //cout<<u<<endl;
                Q.pop();
                for (int i=Head[u];i!=-1;i=Next[i])
                {
                    if ((W[i]>0)&&(Depth[V[i]]==0))
                    {
                        Depth[V[i]]=Depth[u]+1;
                        Q.push(V[i]);
                    }
                }
            }
            while (!Q.empty());
            //cout<<"Bfs.end"<<endl;
            if (Depth[t]>0)
                return 1;
            return 0;
        }
    int Dinic()
        {
            int Ans=0;
            while (bfs())
            {
                while (int d=dfs(s,inf))
                    Ans+=d;
            }
            return Ans;
        }
};

Dinic 算法的優化

Dinic 算法還有優化,這個優化被稱爲當前弧優化,即每一次 dfs 增廣時不從第一條邊開始,而是用一個數組 cur 記錄點 u 之前循環到了哪一條邊,以此來加速
總代碼如下,修改的地方已在代碼中標出:

class Graph
{
private:
    int cnt;
    int Head[maxN];
    int Next[maxM];
    int W[maxM];
    int V[maxM];
    int Depth[maxN];
    int cur[maxN];//cur就是記錄當前點u循環到了哪一條邊
public:
    int s,t;
    void init()
        {
            cnt=-1;
            memset(Head,-1,sizeof(Head));
            memset(Next,-1,sizeof(Next));
        }
    void _Add(int u,int v,int w)
        {
            cnt++;
            Next[cnt]=Head[u];
            Head[u]=cnt;
            V[cnt]=v;
            W[cnt]=w;
        }
    void Add_Edge(int u,int v,int w)
        {
            _Add(u,v,w);
            _Add(v,u,0);
        }
    int dfs(int u,int flow)
        {
            if (u==t)
                return flow;
            for (int& i=cur[u];i!=-1;i=Next[i])//注意這裏的&符號,這樣i增加的同時也能改變cur[u]的值,達到記錄當前弧的目的
            {
                if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))
                {
                    int di=dfs(V[i],min(flow,W[i]));
                    if (di>0)
                    {
                        W[i]-=di;
                        W[i^1]+=di;
                        return di;
                    }
                }
            }
            return 0;
        }
    int bfs()
        {
            queue<int> Q;
            while (!Q.empty())
                Q.pop();
            memset(Depth,0,sizeof(Depth));
            Depth[s]=1;
            Q.push(s);
            do
            {
                int u=Q.front();
                Q.pop();
                for (int i=Head[u];i!=-1;i=Next[i])
                    if ((Depth[V[i]]==0)&&(W[i]>0))
                    {
                        Depth[V[i]]=Depth[u]+1;
                        Q.push(V[i]);
                    }
            }
            while (!Q.empty());
            if (Depth[t]>0)
                return 1;
            return 0;
        }
    int Dinic()
        {
            int Ans=0;
            while (bfs())
            {
                for (int i=1;i<=n;i++)//每一次建立完分層圖後都要把cur置爲每一個點的第一條邊 感謝@青衫白敘指出這裏之前的一個疏漏
                    cur[i]=Head[i];
                while (int d=dfs(s,inf))
                {
                    Ans+=d;
                }
            }
            return Ans;
        }
};

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章