【網絡流】學習筆記:一次理解網絡流!

一、從概念入手


網絡流用於解決流量問題

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

  • 定義:帶權的有向圖G=(V,E),滿足以下條件,則稱爲網絡流圖(flow network):

  1. 僅有一個入度爲0的頂點s,稱s爲源點
  2. 僅有一個出度爲0的頂點t,稱t爲匯點
  3. 每條邊的權值都爲非負數,稱爲該邊的容量,記作c(i,j)。

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

性質

對於任意一個時刻,設f(u,v)實際流量,則整個圖G的流網絡滿足3個性質:

  1. 容量限制:對任意u,v∈V,f(u,v)≤c(u,v)。
  2. 反對稱性:對任意u,v∈V,f(u,v) = -f(v,u)。從u到v的流量一定是從v到u的流量的相反值。
  3. 流守恆性:對任意u,若u不爲S或T,一定有∑f(u,v)=0,(u,v)∈E。即u到相鄰節點的流量之和爲0,因爲流入u的流量和u點流出的流量相等,u點本身不會”製造”和”消耗”流量。

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

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

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

這裏寫圖片描述
上圖中的可行流即爲(2+1+2=)5

零流 若網絡流上每條弧上的流量都爲0,則該網絡流稱爲零流.
僞流:如果一個網絡流只滿足弧流量限制條件,不滿足平衡條件,則這種網絡流爲僞流,或稱爲容量可行流.(預流推進算法有用)

最大流最小割定理:

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

最大流

對於網絡流圖G,流量最大的可行流f,稱爲最大流

的類型:
  1. a.飽和弧:即f(u,v)=c(u,v);
  2. b.非飽和弧:即f(u,v) < c(u,v);
  3. c.零流弧:即f(u,v)=0;
  4. d.非零流弧:即f(u,v)>0.

最大流最小,可行流爲最大流,當且僅當不存在新的增廣路徑。

:

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

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

這裏寫圖片描述
無向圖的割集(Cut Set):C[A,B]是將圖G分爲A和B兩個點集 A和B之間的邊的全集
網絡的割集:C[S,T]是將網絡G分爲s和t兩部分點集 S屬於s且T屬於t 從S到T的邊的全集
帶權圖的割(Cut):就是割集中邊或者有向邊的權和

通俗的理解一下: 割集好比是一個恐怖分子 把你家和自來水廠之間的水管網絡砍斷了一些 然後自來水廠無論怎麼放水 水都只能從水管斷口嘩嘩流走了
你家就停水了 割的大小應該是恐怖分子應該關心的事 畢竟細管子好割一些 而最小割花的力氣最小

這裏寫圖片描述

增廣路:

如果一個可行流不是最大流,那麼當前網絡中一定存在一條增廣路
什麼是增廣路?
預備:

a.前向弧:(方向與鏈的正方向一致的弧),其集合記爲P+,
b.後向弧:(方向與鏈的正方向相反的弧),其集合記爲P-.
  • 1
  • 2

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

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

b.P中所有後向弧都是非零弧.
  • 1
  • 2
  • 3

則稱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).

殘餘網絡 (Residual Network)

在一個網絡流圖上,找到一條源到匯的路徑(即找到了一個流量)後,對路徑上所有的邊,其容量都減去此次找到的量,對路徑上所有的邊,都添加一條反向邊,其容量也等於此次找到的流量,這樣得到的新圖,就稱爲原圖的“殘餘網絡”

費用流

這裏寫圖片描述

最小費用最大流

下面介紹網絡流理論中一個最爲重要的定理
最大流最小割定理(Maximum Flow, Minimum Cut Theorem):
網絡的最大流等於最小割

具體的證明分三部分
1.任意一個流都小於等於任意一個割 這個很好理解 自來水公司隨便給你家通點水 構成一個流 恐怖分子隨便砍幾刀 砍出一個割 由於容量限制 每一根的被砍的水管子流出的水流量都小於管子的容量 每一根被砍的水管的水本來都要到你家的 現在流到外面 加起來得到的流量還是等於原來的流
管子的容量加起來就是割 所以流小於等於割 由於上面的流和割都是任意構造的 所以任意一個流小於任意一個割
2.構造出一個流等於一個割 當達到最大流時 根據增廣路定理 殘留網絡中s到t已經沒有通路了 否則還能繼續增廣 我們把s能到的的點集設爲S 不能到的點集爲T 構造出一個割集C[S,T] S到T的邊必然滿流 否則就能繼續增廣 這些滿流邊的流量和就是當前的流即最大流
把這些滿流邊作爲割 就構造出了一個和最大流相等的割
3.最大流等於最小割 設相等的流和割分別爲Fm和Cm 則因爲任意一個流小於等於任意一個割 任意F≤Fm=Cm≤任意C 定理說明完成,證明如下: 對於一個網絡流圖G=(V,E),其中有源點s和匯點t,那麼下面三個條件是等價的:
1. 流f是圖G的最大流
2. 殘留網絡Gf不存在增廣路
3. 對於G的某一個割(S,T),此時f = C(S,T) 首先證明1 => 2:

我們利用反證法,假設流f是圖G的最大流,但是殘留網絡中還存在有增廣路p,其流量爲fp。則我們有流f’=f+fp>f。這與f是最大流產生矛盾。
接着證明2 => 3:

假設殘留網絡Gf不存在增廣路,所以在殘留網絡Gf中不存在路徑從s到達t。我們定義S集合爲:當前殘留網絡中s能夠到達的點。同時定義T=V-S。
此時(S,T)構成一個割(S,T)。且對於任意的u∈S,v∈T,有f(u,v)=c(u,v)。若f(u,v) < c(u,v),則有Gf(u,v) > 0,s可以到達v,與v屬於T矛盾。
因此有f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T)。 最後證明3 => 1:

由於f的上界爲最小割,當f到達割的容量時,顯然就已經到達最大值,因此f爲最大流。 這樣就說明了爲什麼找不到增廣路時,所求得的一定是最大流。

這篇文章對理解概念也是不錯的。

好了概念就講到這裏,下面看一看具體的算法

二、網絡流常用算法


一、最大流算法

下面是所有最大流算法的精華部分:引入反向邊
爲什麼要有反向邊呢?
這裏寫圖片描述
我們第一次找到了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,等於沒有流量。

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

(1)Edmonds-Karp算法

原理

求最大流的過程,就是不斷找到一條源到匯的路徑,若有,找出增廣路徑上每一段[容量-流量]的最小值delta,然後構建殘餘網絡,再在殘餘網絡上尋找新的路徑,使總流量增加。然後形成新的殘餘網絡,再尋找新路徑…..直到某個殘餘網絡上找不到從源到匯的路徑爲止,最大流就算出來了。

先從Ford-Fulkerson算法看起?

現在假設每條邊的容量都是整數。這個算法每次都能將流至少增加1。由於整個網絡的流量最多不超過圖中所有的邊的容
量和C,從而算法會結束 。
這個算法實現很簡單但是注意到在圖中C可能很大很大
比如說下面這張圖

如果運氣不好這種圖會讓你的程序執行200次dfs,雖然實際上最少只要2次我們就能得到最大流
這裏寫圖片描述
如何避免上述的情況發生?

在每次增廣的時候,選擇從源到匯的具有最少邊數的增廣路徑,也就是說!不是通過dfs尋找增廣路徑,而是通過bfs尋找增廣路徑。
這就是Edmonds-Karp 最短增廣路算法
已經證明這種算法的複雜度上限爲nm2 (n是點數,m是邊數)

代碼

板子題 :codevs1933 poj1273

//codevs 1993
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int INF=0x7ffffff;

queue <int> q;
int n,m,x,y,s,t,g[201][201],pre[201],flow[201],maxflow; 
//g鄰接矩陣存圖,pre增廣路徑中每個點的前驅,flow源點到這個點的流量 

inline int bfs(int s,int t)
{
    while (!q.empty()) q.pop();
    for (int i=1; i<=n; i++) pre[i]=-1;
    pre[s]=0;
    q.push(s);
    flow[s]=INF;
    while (!q.empty())
    {
        int x=q.front();
        q.pop();
        if (x==t) break;
        for (int i=1; i<=n; i++)
          //EK一次只找一個增廣路 
          if (g[x][i]>0 && pre[i]==-1)
          {
            pre[i]=x;
            flow[i]=min(flow[x],g[x][i]);
            q.push(i);
          }
    }
    if (pre[t]==-1) return -1;
    else return flow[t];
}

//increase爲增廣的流量 
void EK(int s,int t)
{
    int increase=0;
    while ((increase=bfs(s,t))!=-1)//這裏的括號加錯了!Tle 
    {//迭代 
        int k=t;
        while (k!=s)
        {
            int last=pre[k];//從後往前找路徑
            g[last][k]-=increase;
            g[k][last]+=increase;
            k=last;
        }
        maxflow+=increase;
    }
}

int main()
{
    scanf("%d%d",&m,&n);
    for (int i=1; i<=m; i++)
    {
        int z;
        scanf("%d%d%d",&x,&y,&z);
        g[x][y]+=z;//此處不可直接輸入,要+= 
    }
    EK(1,n);
    printf("%d",maxflow);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

(2) Dinic算法

前面的網絡流算法,每進行一次增廣,都要做 一遍BFS,十分浪費。能否少做幾次BFS?
這就是Dinic算法要解決的問題

原理

dinic算法在EK算法的基礎上增加了分層圖的概念,根據從s到各個點的最短距離的不同,把整個圖分層。尋找的增廣路要求滿足所有的點分別屬於不同的層,且若增廣路爲s,P1,P2…Pk,t,點v在分層圖中的所屬的層記爲deepv,那麼應滿足deeppi=deeppi−1+1

Edmonds-Karp的提高餘地:需要多次從s到t調用BFS,可以設法減少調用次數。 亦即:使用一種代價較小的高效增廣方法。考慮:在一次增廣的過程中,尋找多條增廣路徑。
DFS

算法流程

  • 先利用BFS對殘餘網絡分層
    一個節點的深度,就是源點到它最少要經過的邊數。
    這裏寫圖片描述
  • 利用BFS對殘餘網絡分層,分完層後,利用DFS從前一層向後一層反覆尋找增廣路。
    這裏寫圖片描述

  • 分完層後,從源點開始,用DFS從前一層向後一層反覆尋找增廣路(即要求DFS的每一步都必須要走到下一層的節點)。
    因此,前面在分層時,只要進行到匯點的層次數被算出即可停止,因爲按照該DFS的規則,和匯點同層或更下一層的節點,是不可能走到匯點的。
  • DFS過程中,要是碰到了匯點,則說明找到了一條增廣路徑。此時要增加總流量的值,消減路徑上各邊的容量,並添加反向邊,即所謂的進行增廣。

  • DFS找到一條增廣路徑後,並不立即結束,而是回溯後繼續DFS尋找下一個增廣路徑。
    回溯到哪個節點呢?
    回溯到的節點u滿足以下條件:

  1. DFS搜索樹的樹邊(u,v)上的容量已經變成0。即剛剛找到的增廣路徑上所增加的流量,等於(u,v)本次增廣前的容量。(DFS的過程中,是從u走到更下層的v的)
  2. u是滿足條件 1)的最上層的節點如果回溯到源點而且無法繼續往下走了,DFS結束。
    因此,一次DFS過程中,可以找到多條增廣路徑。
  • DFS結束後,對殘餘網絡再次進行分層,然後再進行DFS當殘餘網絡的分層操作無法算出匯點的層次(即BFS到達不了匯點)時,算法結束,最大流求出。
  • ps要求出最大流中每條邊的流量,怎麼辦?

    將原圖備份,原圖上的邊的容量減去做完最大流的殘餘網絡上的邊的剩餘容量,就是邊的流量。

    時間複雜度

    在普通情況下, DINIC算法時間複雜度爲O(V2E) 
    在二分圖中, DINIC算法時間複雜度爲O(√VE)
    
    • 1
    • 2

    優化

    • 多路增廣
    每次不是尋找一條增廣路,而是在DFS中,只要可以就遞歸增廣下去,實際上形成了一張增廣網。
    • 當前弧優化
    對於每一個點,都記錄上一次檢查到哪一條邊。因爲我們每次增廣一定是徹底增廣(即這條已經被增廣過的邊已經發揮出了它全部的潛力,不可能再被增廣了),下一次就不必再檢查它,而直接看第一個未被檢查的邊。

    優化之後漸進時間複雜度沒有改變,但是實際上能快不少。
    實際寫代碼的時候要注意,head數組初始值爲-1,存儲時從0開始存儲,這樣在後面寫反向弧的時候比較方便,直接異或即可。
    關於複製head的數組cur;目的是爲了當前弧優化。已經增廣的邊就不需要再走了.

    代碼

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    using namespace std;
    const int inf=1e9;
    
    int n,m,x,y,z,maxflow,deep[500];//deep深度 
    struct Edge{
        int next,to,dis;
    }edge[500];
    int num_edge=-1,head[500],cur[500];//cur用於複製head 
    queue <int> q;
    
    void add_edge(int from,int to,int dis,bool flag)
    {
        edge[++num_edge].next=head[from];
        edge[num_edge].to=to;
        if (flag) edge[num_edge].dis=dis;//反圖的邊權爲 0
        head[from]=num_edge;
    }
    
    //bfs用來分層 
    bool bfs(int s,int t)
    {
        memset(deep,0x7f,sizeof(deep));
        while (!q.empty()) q.pop();
        for (int i=1; i<=n; i++) cur[i]=head[i];
        deep[s]=0;
        q.push(s);
    
        while (!q.empty())
        {
            int now=q.front(); q.pop();
            for (int i=head[now]; i!=-1; i=edge[i].next)
            {
                if (deep[edge[i].to]>inf && edge[i].dis)//dis在此處用來做標記 是正圖還是返圖 
                {
                    deep[edge[i].to]=deep[now]+1;
                    q.push(edge[i].to);
                }
            }
        }
        if (deep[t]<inf) return true;
        else return false;
    }
    
    //dfs找增加的流的量 
    int dfs(int now,int t,int limit)//limit爲源點到這個點的路徑上的最小邊權 
    {
        if (!limit || now==t) return limit;
    
        int flow=0,f;
        for (int i=cur[now]; i!=-1; i=edge[i].next)
        {
            cur[now]=i;
            if (deep[edge[i].to]==deep[now]+1 && (f=dfs(edge[i].to,t,min(limit,edge[i].dis))))
            {
                flow+=f;
                limit-=f;
                edge[i].dis-=f;
                edge[i^1].dis+=f;
                if (!limit) break;
            }
        }
        return flow;
    }
    
    void Dinic(int s,int t)
    {
        while (bfs(s,t))
            maxflow+=dfs(s,t,inf);
    }
    
    int main()
    {
    //  for (int i=0; i<=500; i++) edge[i].next=-1;
        memset(head,-1,sizeof(head));
        scanf("%d%d",&m,&n);
        for (int i=1; i<=m; i++)
        {
            scanf("%d%d%d",&x,&y,&z);
            add_edge(x,y,z,1); add_edge(y,x,z,0);
        }
        Dinic(1,n);
        printf("%d",maxflow);
        return 0;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    (3) ISAP算法

    原理

    ISAP(Improved Shortest Augmenting Path)(%ISA)也是基於分層思想的最大流算法。所不同的是,它省去了Dinic每次增廣後需要重新構建分層圖的麻煩,而是在每次增廣完成後自動更新每個點的『標號』(也就是所在的層)

    最短增廣路算法是一種運用距離標號使尋找增廣路的時間複雜度下降的算法。所謂的距離標號就是某個點到匯點的最少的弧的數量(即當邊權爲1時某個點的最短路徑長度). 設點i的標號爲d[i], 那麼如果將滿足d[i] = d[j] + 1, 且增廣時只走允許弧, 那麼就可以達到”怎麼走都是最短路”的效果. 每個點的初始標號可以在一開始用一次從匯點沿所有反向的BFS求出.

    算法流程

    1. 定義節點的標號爲到匯點的最短距離;
    2. 每次沿可行邊進行增廣, 可行邊即: 假設有兩個點 i, j 若 d[i] = 3, d[j] = 4, 則d[j] = d[i] + 1, 也就是從 j 到 i 有一條邊.
    3. 找到增廣路後,將路徑上所有邊的流量更新.
    4. 遍歷完當前結點的可行邊後更新當前結點的標號爲 d[now] = min( d[next] , add_flow(now,next) > 0)+1,使下次再搜的時候有路可走。
    5. 圖中不存在增廣路後即退出程序,此時得到的流量值就是最大流。

    需要注意的是, 標號的更新過程首先我們要理解更新標號的目的。標號如果需要更新,說明在當前的標號下已經沒有增廣路可以繼續走,這時更新標號就可以使得我們有繼續向下走的可能,並且每次找的都是能走到的點中標號最小的那個點,這樣也使得每次搜索長度最小.

    GAP 優化

    由於可行邊定義爲:(now,next) | h[now] = h[next]+1,所以若標號出現“斷層”即有的標號對應的頂點個數爲0,則說明剩餘圖中不存在增廣路,此時便可以直接退出,降低了無效搜索。舉個栗子:若結點標號爲3的結點個數爲0,而標號爲4的結點和標號爲2的結點都大於 0,那麼在搜索至任意一個標號爲4的結點時,便無法再繼續往下搜索,說明圖中就不存在增廣路。此時我們可以以將 h[1]=n 形式來變相地直接結束搜索

    時間複雜度

    漸進時間複雜度和dinic相同,但是非二分圖的情況下isap更具優勢。

    代碼

    當代碼不能理解時,就開啓DeBug自己手推一遍,有利於理解的更深刻,也有利於記憶模板(口胡

    //codevs上還是WA一個點 
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<iostream>
    #include<queue>
    using namespace std;
    const int inf=1e9;
    
    queue <int> q;
    int m,n,x,y,z,maxflow,head[5000],num_edge=-1;
    int cur[5000],deep[5000],last[5000],num[5000];
    //cur當前弧優化; last該點的上一條邊; num桶 用來GAP優化 
    struct Edge{
        int next,to,dis;
    }edge[500];
    
    void add_edge(int from,int to,int dis,bool flag)
    {
        edge[++num_edge].next=head[from];
        edge[num_edge].to=to;
        edge[num_edge].dis=dis;
        head[from]=num_edge;
    }
    
    //bfs僅用於更新deep 
    void bfs(int t)
    {
        while (!q.empty()) q.pop();
        for (int i=0; i<=m; i++) cur[i]=head[i];
        for (int i=1; i<=n; i++) deep[i]=n;
        deep[t]=0;
        q.push(t);
    
        while (!q.empty())
        {
            int now=q.front(); q.pop();
            for (int i=head[now]; i!=-1; i=edge[i].next)
            {
                if (deep[edge[i].to]==n && edge[i^1].dis)//i^1是爲了找反邊 
                {
                    deep[edge[i].to]=deep[now]+1;
                    q.push(edge[i].to);
                }
            }
        }
    }
    
    int add_flow(int s,int t)
    {
        int ans=inf,now=t;
        while (now!=s)
        {
            ans=min(ans,edge[last[now]].dis);
            now=edge[last[now]^1].to;
        }
        now=t;
        while (now!=s)
        {
            edge[last[now]].dis-=ans;
            edge[last[now]^1].dis+=ans;
            now=edge[last[now]^1].to;
        }
        return ans;
    }
    
    void isap(int s,int t)
    {
        int now=s;
        bfs(t);//搜出一條增廣路
        for (int i=1; i<=n; i++) num[deep[i]]++;
    
        while (deep[s]<n)
        {
            if (now==t)
            {//如果到達匯點就直接增廣,重新回到源點進行下一輪增廣 
                maxflow+=add_flow(s,t);
                now=s;//回到源點 
            }
    
            bool has_find=0;
            for (int i=cur[now]; i!=-1; i=edge[i].next)
            {
                if (deep[now]==deep[edge[i].to]+1 && edge[i].dis)//找到一條增廣路 
                {
                    has_find=true;
                    cur[now]=i;//當前弧優化
                    now=edge[i].to;
                    last[edge[i].to]=i;
                    break;
                }
            }
    
            if (!has_find)//沒有找到出邊,重新編號 
            {
                int minn=n-1;
                for (int i=head[now]; i!=-1; i=edge[i].next)//回頭找路徑 
                    if (edge[i].dis)
                        minn=min(minn,deep[edge[i].to]);
                if ((--num[deep[now]])==0) break;//GAP優化 出現了斷層 
                num[deep[now]=minn+1]++;
                cur[now]=head[now];
                if (now!=s)
                    now=edge[last[now]^1].to;
            }
        }
    }
    
    int main()
    {
        memset(head,-1,sizeof(head));
        scanf("%d%d",&m,&n);
        for (int i=1; i<=m; i++)
        {
            scanf("%d%d%d",&x,&y,&z);
            add_edge(x,y,z,1); add_edge(y,x,z,0); 
        }
        isap(1,n);
        printf("%d",maxflow);
        return 0;
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    對於EK算法與ISAP算法的區別:
    EK算法每次都要重新尋找增廣路,尋找過程只受殘餘網絡的影響,如果改變殘餘網絡,則增廣路的尋找也會隨之改變;SAP算法預處理出了增廣路的尋找大致路徑,若中途改變殘餘網絡,則此算法將重新進行。EK處理在運算過程中需要不斷加邊的最大流比SAP更有優勢。

    二、費用流算法

    現在我們想象假如我們有一個流量網絡,現在每個邊除了流量,現在還有一個單位費用,這條邊的費用相當於它的單位費用乘上它的流量,我們要保持最大流的同時,還要保持邊權最小,這就是最小費用最大流問題。
    因爲在一個網絡流圖中,最大流量只有一個,但是“流法”有很多種,每種不同的流法所經過的邊不同因此費用也就不同,所以需要用到最短路算法。
    總增廣的費用就是最短路*總流量

    (1)SPFA

    就是把Dinic中的bfs改成spfa,再求最大流的過程中最小費用流也就求出來了。
    有許多初始化的地方容易漏易錯;
    flow和dis容易混易錯;
    模板題 luoguP3381

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    using namespace std;
    const int maxn=100010;
    
    bool vis[maxn];
    int n,m,s,t,x,y,z,f,dis[maxn],pre[maxn],last[maxn],flow[maxn],maxflow,mincost;
    //dis最小花費;pre每個點的前驅;last每個點的所連的前一條邊;flow源點到此處的流量 
    struct Edge{
        int to,next,flow,dis;//flow流量 dis花費 
    }edge[maxn];
    int head[maxn],num_edge; 
    queue <int> q;
    
    void add_edge(int from,int to,int flow,int dis)
    {
        edge[++num_edge].next=head[from];
        edge[num_edge].to=to;
        edge[num_edge].flow=flow;
        edge[num_edge].dis=dis;
        head[from]=num_edge;
    }
    
    bool spfa(int s,int t)
    {
        memset(dis,0x7f,sizeof(dis));
        memset(flow,0x7f,sizeof(flow));
        memset(vis,0,sizeof(vis));
        q.push(s); vis[s]=1; dis[s]=0; pre[t]=-1;
    
        while (!q.empty())
        {
            int now=q.front();
            q.pop();
            vis[now]=0;
            for (int i=head[now]; i!=-1; i=edge[i].next)
            {
                if (edge[i].flow>0 && dis[edge[i].to]>dis[now]+edge[i].dis)//正邊 
                {
                    dis[edge[i].to]=dis[now]+edge[i].dis;
                    pre[edge[i].to]=now;
                    last[edge[i].to]=i;
                    flow[edge[i].to]=min(flow[now],edge[i].flow);//
                    if (!vis[edge[i].to])
                    {
                        vis[edge[i].to]=1;
                        q.push(edge[i].to);
                    }
                }
            }
        }
        return pre[t]!=-1;
    }
    
    void MCMF()
    {
        while (spfa(s,t))
        {
            int now=t;
            maxflow+=flow[t];
            mincost+=flow[t]*dis[t];
            while (now!=s)
            {//從源點一直回溯到匯點 
                edge[last[now]].flow-=flow[t];//flow和dis容易搞混 
                edge[last[now]^1].flow+=flow[t];
                now=pre[now];
            }
        }
    }
    
    int main()
    {
        memset(head,-1,sizeof(head)); num_edge=-1;//初始化 
        scanf("%d%d%d%d",&n,&m,&s,&t);
        for (int i=1; i<=m; i++)
        {
            scanf("%d%d%d%d",&x,&y,&z,&f);
            add_edge(x,y,z,f); add_edge(y,x,0,-f);
            //反邊的流量爲0,花費是相反數 
        }
        MCMF();
        printf("%d %d",maxflow,mincost);
        return 0;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    (2)Dijstra(待更新)

    三、最小割算法

    Stoer_WagnerStoer_Wagner 算法
    首先回顧一下前面的幾個定義

    :在一個圖G(V,E)中V是點集,E是邊集。在E中去掉一個邊集C使得G(V,E-C)不連通,C就是圖G(V,E)的一個割;
    最小割:在G(V,E)的所有割中,邊權總和最小的割就是最小割。
    最大流最小割定理:
    1. 最小割等價於最大流。
    2. 最小割在最大流中一定是滿流邊,是增廣路徑中容量最小的邊。
    3. 一條增廣路徑只對應一條最小割。(如果一條增廣路中兩條滿流且都需要割掉,那一定通過反向邊分成兩條增廣路)

    最小割在最大流中一定是滿流邊,其實就是S到T之間必須經過的邊(不管確定與否)。只要找到一條增廣路徑,就必須經過一條最小割。最小割中的邊飽和就再也不能找到增廣路徑。
    一條最小割可以對應多條增廣路徑,但是一條增廣路徑只能對應一條最小割(或最小割的可能性)。
    求最小割其實就是一條增廣路徑中容量最小的邊,這恰好與最大流的求解是一致的。

    算法原理與過程

    Stoer_Wagner算法是求無向圖全局最小割的一個有效算法,最壞時間複雜度O(n3),主要思想是先找任意2點的最小割,然後記錄下這個最小割,再合併這2個點。這樣經過n−1次尋找任意2點最小割,每次更新全局最小割,最後整張圖縮成一個點,算法結束,所保存下來的最小割就是全局最小割。

    Stoer−Wagner的正確性:
    設S和T是圖G的2個頂點,圖G的全局最小割要麼是S−T的最小割,此時S和T在G的全局最小割的2個不同的子集中,要麼就是G中將S和T合併得的的新圖G′的全局最小割,此時S和T在G的全局最小割的同一個子集中。所以只需要不斷求出當前圖中任意2個點的最小割,然後合併這2個點。不斷縮小圖的規模求得最小割。

    見圖片
    這裏寫圖片描述這裏寫圖片描述

    複雜度

    算法複雜度爲O(n3)。如果在prim中加堆優化,複雜度會降爲O(n2logn)。

    代碼

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    const int maxn=520;
    const int inf=1e9;
    
    int n,m,mp[maxn][maxn],v[maxn],dis[maxn];
    //mp圖,v[i]表示i節點合併到的頂點;dis[i] i點到A集合中所有的點的長度之和 
    bool vis[maxn];//是否進入集合A 
    
    int SW(int n)
    {
        int ans=inf;
        for (int i=0; i<n; i++) v[i]=i;//頂點定爲自己 
        while (n>1)
        {
            int k=1,pre=0;//k保存最大dis值的下標 pre上一次選入集合的點 
            //每次使0爲第一個加入的點 
            for (int i=1; i<n; i++)
            {
                dis[v[i]]=mp[v[0]][v[i]];
                if (dis[v[i]]>dis[v[k]]) k=i;
            }
            memset(vis,0,sizeof(vis));
            vis[v[0]]=1;//標記進入集合 
            for (int i=1; i<n; i++)
            {
                if (i==n-1)//最後一次加入,更新答案 
                {
                    ans=min(ans,dis[v[k]]);
                    for (int j=0; j<n; j++)
                    {
                        mp[v[pre]][v[j]] += mp[v[j]][v[k]];
                        mp[v[j]][v[pre]] += mp[v[j]][v[k]];
                    }
                    v[k]=v[--n];//刪除最後一個點 
                }
                vis[v[k]]=1;
                pre=k;
                k=-1;
                for (int j=0; j<n; j++)
                    if (!vis[v[j]])
                    { 
                        //將上一次求的k加入集合 
                        dis[v[j]]+=mp[v[pre]][v[j]];
                        if (k==-1 || dis[v[j]]>dis[v[k]])
                            k=j;
                    }
            }
        }
        return ans;
    }
    
    int main()
    {
        while (~scanf("%d%d",&n,&m))
        {
            memset(mp,0,sizeof(mp));
            int x,y,z;
            for (int i=1; i<=m; i++)
            {
                scanf("%d%d%d",&x,&y,&z);
                mp[x][y]+=z; mp[y][x]+=z;
            }
            printf("%d\n",SW(n));
        }
        return 0;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    四、二分圖

    一個無向圖是二分圖的充要條件是不存在奇環。

    常用技巧:棋盤圖黑白染色形成二分圖&&網格x-y形成二分圖。

    S向左側xi連邊,右側yi向T連邊。

    二分圖題目先轉化成對應二分圖模型,再對應轉化爲網絡流。

    標準建圖:邊權全爲1。

    1.最大匹配=最大流(最大匹配就是匹配邊數最大)

    2.最小覆蓋=最小割(最小覆蓋就是選擇最少的點使每條邊至少有一個端點被選中,選點類比割邊就是最小割)

    3.最大獨立集=總點數-最小覆蓋(最大獨立集就是選擇最多的點使每條邊至少有一個點不被選中。獨立集中不能有邊,每個兩點匹配只能選一點,所以把最大匹配數減去後就是答案。)

    4.最小路徑覆蓋:在DAG找儘量少的路徑,使每個節點恰好在一條路徑上(點不相交)。

    做法:將每個點拆開分別放入xy集合中,如果u到v有一條邊,則連邊u,v`,然後二分圖最大匹配。

    初始未匹配ans=n即每個點單獨爲一條路徑,匹配一條說明連了兩點,ans-1,所以最終ans=N-最大匹配。

    5.二分圖帶權匹配:要求完美匹配就跑費用流,不要求完美匹配就跑流量不固定的費用流(spfa時若最短路費用對答案沒貢獻就返回失敗)。

    【最大權閉合子圖】

    S向正權點連邊,負權點向T連邊,0不管,原邊全部轉爲正無窮(節點權值全部轉到了與S、T的連邊上)。

    hiho 第119周 最大權閉合子圖

    注意:上文中後面證明中的S集是閉合子圖和源點S的集合,T集是其它點和匯點T的集合!

    割掉與S相鄰的邊就是這個點捨棄了S,成爲T中的正權點(離開閉合子圖);割掉與T相鄰的邊就是這個點捨棄T,成爲S中的負權點(進入閉合子圖)。(割的值與答案密切相關)

    因爲每條路徑必須有割,所以對於所有依賴關係要麼與T相鄰斷邊(把依賴對象收進來),要麼與S相鄰斷邊(把依賴源扔掉)。

    最大權閉合子圖的權值=所有正權點之和-最小割

    可以簡單理解爲理想可以收入所有正權點,捨棄所有負權點,然而實際上需要扔些正權點,撿一些負權點(即每條路徑有一割)。

    對於扔掉的正權點,就是減去割去的S鄰邊權值;對於撿起來的負權點,其實就是加上負權=減去割去的T鄰邊的權值。

    所以權值=正權點之和-割,最小割對應最大權閉合子圖。

    參考:http://blog.csdn.net/hbhcy98/article/details/51202000

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