網絡流算法


2018-03-13 19:02:13

圖論中,網絡流英語:Network flow)是指在一個每條邊都有容量(capacity)的有向圖分配流,使一條邊的流量不會超過它的容量。通常在運籌學中,有向圖稱為網絡。頂點稱為節點(node)而邊稱為(arc)。一道流必須匹配一個結點的進出的流量相同的限制,除非這是一個源點(source)──有較多向外的流,或是一個匯點(sink)──有較多向內的流。一個網絡可以用來模擬道路系統的交通量、管中的液體、電路中的電流或類似一些東西在一個結點的網絡中游動的任何事物。

一、最大流最小割定理

最大流最小割定理提供了對於一個網絡流從源點到目標點的最大的流量等於最小割的每一條邊的和

這個定理說明,當網絡達到最大流時,會有一個割集,這個割集中的所有邊都達到飽和狀態。

這等價於在網絡中再也找不到一個從s到t的增廣路徑。

因為只要能找到一條增廣路徑,這條增廣路徑肯定要經過最小割集中的一條邊,否則這個割集就不能稱之為割集了。

既然這個割集中所有的邊都飽和了,因此也就不會存在這樣的增廣路徑了。

這個定理的意義在於給我們指明了方向:

任何算法,只要最后能達到“再也找不到一條增廣路徑”,就可以說明這個算法最后達到了最大流。

 

二、最大流問題

在優化理論中,最大流問題涉及到在一個單源點、單匯點的網絡流中找到一條最大的流。

最大流問題可以被看作是一個更復雜的網絡流問題(如循環問題(circulation problem))的特殊情況,。s-t流(從源點s到匯點t)的最大值等於s-t割的最小容量,這被稱為最大流最小割定理。

下面舉例來說明這個問題:

問題描述:

給定一個有向圖G=(V,E),把圖中的邊看作管道,每條邊上有一個權值,表示該管道的流量上限。給定源點s和匯點t,現在假設在s處有一個水源,t處有一個蓄水池,問從s到t的最大水流量是多少。

這個問題有如下的一些限制:

  • 容量限制:也就是在每條通路上的流量都不能超過其capacity。
  • 平衡條件:除了源點和匯點,其他的點的流入量需要等於流出量。
  • 反對稱:v到u的凈流量等於u到v的凈流量的相反。

問題求解:

方法一、朴素dfs

一種非常朴素的思路是,使用dfs每次尋找s->t的一條通路,然后將這條路上的流量值定義為其中最小的容量值,完成這次運輸后,將通路上的所有邊的容量值減去流量值,開始下一次的尋找,直到沒有通路,完成算法。

這個算法看上去是沒有什么問題的,因為每次都在增加流量直到不能增加為止,可是真的是這樣么?

我們看下面的這個例子:

如果第一次dfs到的通路是s - > a - > b - > t,那么這次之后,網絡中就再也沒有從s - > t的通路了,按照算法的思路,這個網絡的最大流就是100,然而,很明顯的,這個網絡的最大流是200。

因此,簡單的使用dfs是不能很好的解決這個問題的,下面的Ford-Fulkerson算法解決了這個問題,而成為了網絡流算法中的經典。

 

方法二、Ford-Fulkerson算法

在上面的例子中出錯的原因是a - > b 的實際流量應該是0,但是我們過早的認為他們之間是有流量的,因此封鎖了我們最大流繼續增大的可能。

一個改進的思路:應能夠修改已建立的流網絡,使得“不合理”的流量被刪掉。

一種實現:對上次dfs 時找到的流量路徑上的邊,添加一條“反向”邊,反向邊上的容量等於上次dfs 時找到的該邊上的流量,然后再利用“反向”的容量和其他邊上剩余的容量尋找路徑。

下面我們重新對上面的例子進行分析:

1、第一次dfs依然是s - > a - > b - > t,與上次不同的是,這次我們會添加上反向邊。

2、繼續在新圖上找可能通路,我們這次可以找到另一條流量為100的通路s - > b - > a - > t,從宏觀上來看,反向邊起到了“取消流”的功能,也可以看成是兩條通路的合並。

3、對第二次找到的路徑添加反向邊,我們發現圖中已經沒有從s - > t的通路了,於是該網絡的最大流就是200。

添加反向邊的證明:

構建殘余網絡時添加反向邊a->b,容量是n,增廣的時候發現了流量n-k,即新增了n-k的流量。這n-k的流量,從a進,b出,最終流到匯。

現要證明這2n-k的從流量,在原圖上確實是可以從源流到匯的。

在原圖上可以如下分配流量,則能有2n-k從源流到匯點:

 

三、Ford-Fulkerson算法

Ford-Fulkerson算法:求最大流的過程,就是不斷找到一條源到匯的路徑,然后構建殘余網絡,再在殘余網絡上尋找新的路徑,使 總流量增加,然后形成新的殘余網絡,再尋找新路徑….. 直到某個殘余網絡上找不到從源到匯的路徑為止,最大流就算出來了。

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

增廣路徑: 每次尋找新流量並構造新殘余網絡的過程,就叫做尋找流量的“增廣路徑”,也叫“增廣”。

算法時間復雜度分析:現在假設每條邊 的容量都是整數,這個算法每次都能將流至少增加1。由於整個網絡的流量最多不超過  圖中所有的邊的容量和C,從而算法會結束現在來看復雜度找增廣路徑的算法可以用dfs,復雜度為O(V + E)。dfs最多運行C次所以時間復雜度為C*(V + E) =C * V ^ 2。

 

四、Edmonds-Karp算法

這個算法實現很簡單但是注意到在圖中C可能很大很大,比如說下面這張圖,如果運氣不好 這種圖會讓你的程序執行200次dfs,雖然實際上最少只要2次我們就能得到最大流。為了避免這個問題,我們每次在尋找增廣路徑的時候都按照bfs進行尋找,而不是按照dfs進行尋找,這就是Edmonds-Karp最短增廣路算法。

時間復雜度:已經證明這種算法的復雜度上限為O(V*E^2)。

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;


// POJ 1273
public class EdmondsKarp {
    static int[][] G = new int[300][300];
    static boolean[] visited = new boolean[300];
    static int[] pre = new int[300];
    static int v;
    static int e;

    static int augmentRoute() {
        Arrays.fill(visited, false);
        Arrays.fill(pre, 0);
        Queue<Integer> queue = new LinkedList<Integer>();
        queue.add(1);
        boolean flag = false;
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            visited[cur] = true;
            for (int i = 1; i <= v; i++) {
                if (i == cur) continue;
                if (G[cur][i] != 0 && !visited[i]) {
                    pre[i] = cur;
                    if (i == v) {
                        flag = true;
                        queue.clear();
                        break;
                    }
                    else queue.add(i);
                }
            }
        }

        if (!flag) return 0;

        int minFlow = Integer.MAX_VALUE;
        int tmp = v;
        while (pre[tmp] != 0) {
            minFlow = Math.min(minFlow, G[pre[tmp]][tmp]);
            tmp = pre[tmp];
        }

        tmp = v;
        while (pre[tmp] != 0) {
            G[pre[tmp]][tmp] -= minFlow;
            G[tmp][pre[tmp]] += minFlow;
            tmp = pre[tmp];
        }
        return minFlow;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            int res = 0;
            for (int i = 0; i < G.length; i++) {
                Arrays.fill(G[i], 0);
            }
            e = sc.nextInt();
            v = sc.nextInt();
            int s, t, c;
            for (int i = 0; i < e; i++) {
                s = sc.nextInt();
                t = sc.nextInt();
                c = sc.nextInt();
                G[s][t] += c;
            }
            int aug;
            while ((aug = augmentRoute()) != 0) {
                res += aug;
            }

            System.out.println(res);
        }
    }
}

 

五、Dinic算法

Edmonds-Karp已經是一種很不錯的改進方案了,但是每次生成一條增廣路徑都需要對s到t調用BFS,如果能在一次增廣的過程中,尋找到多條增廣路徑,就可以進一步提高算法的運行效率。

Dinic算法就是使用了BFS + DFS達到了以上的思路,完成了算法復雜度的進一步下降。

時間復雜度:O(V^2 * E)

算法流程:

1、使用BFS對殘余網絡進行分層,在分層時,只要進行到匯點的層次數被算出即可停止,因為按照該DFS的規則,和匯點同層或更下一層的節點,是不可能走到匯點的。

2、分完層后,從源點開始,用DFS從前一層向后一層反復尋找增廣路(即要求DFS的每一步都必須要走到下一層的節點)。

3、DFS過程中,要是碰到了匯點,則說明找到了一條增廣路徑。此時要增加總流量的值,消減路徑上各邊的容量,並添加反向邊,即所謂的進行增廣。

4、DFS找到一條增廣路徑后,並不立即結束,而是回溯后繼續DFS尋找下一個增廣路徑。回溯到的結點滿足以下的條件:

  1) DFS搜索樹的樹邊(u,v)上的容量已經變成0。即剛剛找到的增廣路徑上所增加的流量,等於(u,v)本次增廣前的容量。(DFS的過程中,是從u走到更下層的v的)
  2) u是滿足條件 1)的最上層的節點。

5、如果回溯到源點而且無法繼續往下走了,DFS結束。因此,一次DFS過程中,可以找到多條增廣路徑。
6、DFS結束后,對殘余網絡再次進行分層,然后再進行DFS當殘余網絡的分層操作無法算出匯點的層次(即BFS到達不了匯點)時,算法結束,最大流求出。
一般用棧實現DFS,這樣就能從棧中提取出增廣路徑。

// POJ 1273
public class Dinic { static int[][] G = new int[300][300]; static boolean[] visited = new boolean[300]; static int[] layer = new int[300]; static int v; static int e; static boolean countLayer() { int depth = 0; Arrays.fill(layer, -1); layer[1] = 0; Queue<Integer> queue = new LinkedList<Integer>(); queue.add(1); while (!queue.isEmpty()) { int cur = queue.poll(); for (int i = 1; i <= v; i++) { if (G[cur][i] > 0 && layer[i] == -1) { layer[i] = layer[cur] + 1; if (i == v) { queue.clear(); return true; } queue.add(i); } } } return false; } static int dinic() { int res = 0; List<Integer> stack = new ArrayList<Integer>(); while (countLayer()) { stack.add(1); Arrays.fill(visited, false); visited[1] = true; while (!stack.isEmpty()) { int cur = stack.get(stack.size() - 1); if (cur == v) { int minFlow = Integer.MAX_VALUE; int minS = Integer.MAX_VALUE; for (int i = 1; i < stack.size(); i++) { int tmps = stack.get(i - 1); int tmpe = stack.get(i); if (minFlow > G[tmps][tmpe]) { minFlow = G[tmps][tmpe]; minS = tmps; } } // 生成殘余網絡 for (int i = 1; i < stack.size(); i++) { int tmps = stack.get(i - 1); int tmpe = stack.get(i); G[tmps][tmpe] -= minFlow; G[tmpe][tmps] += minFlow; } // 退棧到minS while (!stack.isEmpty() && stack.get(stack.size() - 1) != minS) { stack.remove(stack.size() - 1); } res += minFlow; } else { int i; for (i = 1; i <= v; i++) { if (G[cur][i] > 0 && layer[i] == layer[cur] + 1 && !visited[i]) { visited[i] = true; stack.add(i); break; } } if (i > v) { stack.remove(stack.size() - 1); } } } } return res; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { for (int i = 0; i < G.length; i++) { Arrays.fill(G[i], 0); } e = sc.nextInt(); v = sc.nextInt(); int s, t, c; for (int i = 0; i < e; i++) { s = sc.nextInt(); t = sc.nextInt(); c = sc.nextInt(); G[s][t] += c; } System.out.println(dinic()); } } }

 

六、最小費用最大流

問題描述:

設有一個網絡圖 G(V,E) , V={s,a,b,c,…,s ’},E 中的每條邊 (i,j) 對應一個容量 c(i,j) 與輸送單位流量所需費用a(i,j) 。如有一個運輸方案(可行流),流量為 f(i, j) ,則最小費用最大流問題就是這樣一個求極值問題:

其中 F 為 G 的最大流的集合,即在最大流中尋找一個費用最小的最大流。

算法流程: 

反復用spfa算法做源到匯的最短路進行增廣,邊權值為邊上單位費用。反向邊上的單位費用是負的。直到無法增廣,即為找到最小費用最大流。
成立原因:每次增廣時,每增加1個流量,所增加的費用都是最小的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM