最大流問題和Edmonds-Karp算法

內容概要:

  1. 網絡流與最大流
  2. Ford-Fulkerson思想
  3. Edmonds-Karp算法
  4. 棒球比賽問題

網絡流和最大流

網絡流對應的實際問題有很多,如交通運輸網絡的車輛流,供水系統的水流,金融系統中的現金流,通信系統的負載流等。
給定指定的一個有向圖G,其中有兩個特殊的點:源點S(Sources)和匯點T(Sinks),源點就是入度爲0的點,而匯點是出度爲0的點,圖的每條邊有指定的權值代表最大容量(Capacity),研究這樣的圖的從源點到匯點的流量分配就是網絡流問題。
網絡流的圖論描述
給定一個有向圖G(V,E,C),指定一個源點v_s和匯點v_t,其餘的點叫中間點,對於每一條有向邊<v_i,v_j>\inE,對應有一個權值c_{ij} \in C稱爲該邊的容量。所謂網絡流,是指定義在邊集合E上的函數f=\{f(v_i,v_j)\},並稱f(v_i,v_j)爲邊<v_i,v_j>上的流量,簡記做f_{ij},網絡上流的和記做v(f)
可行流和最大流
從運輸網絡的實際問題中可以看出,對於網絡流有兩個明顯的要求:一是邊上的流量不能超過該邊的最大容量。二是中間點的淨輸出量爲0,因爲對於每個點,運出這點的產品總量與運進這點的產品總量之差爲該點的淨輸出量,中間點只負責轉運,所以淨輸出量必定爲0。同時源點的輸出量和匯點的流入量也一定相等,就是當前網絡的流量。
所以可行流必然滿足:

  • 容量限制:對於每個邊<v_i,v_j>\inE,當前流量不能超過最大容量0 \leq f_{ij} \leq c_{ij}
  • 平衡限制:對於每個中間點v_i,v_j \neq v_s,v_t流出量等於流入量:\sum{f_{ij}}-\sum{f_{ji}}=0。對於源點v_s只有流出:\sum{f_{sj}}-\sum{f_{js}}=v(f),對於匯點v_t只有流入:\sum{f_{tj}}-\sum{f_{jt}}=-v(f),(任意i,j \neq s,t)。

最大流問題就是在可行流的約束下,求一個流f=\{f_{ij}\},使得v(f)達到最大。

Ford-Fulkerson方法

一個最直觀的求解最大流問題的思路就是,逐步向網絡中加入流量,直到網絡中不能再容納更多流量爲止,這種貪心的想法很好,但卻有一定的問題:

如圖示的網絡中,如果填加流量順序不對,有可能得不到正確的結果,在右圖部分,此時網絡已經無法容納更多的容量,但並沒有達到最大流。需要改進我們的貪心方法。
Ford-Fulkerson思想
在上面貪心的基礎上,Ford-Fulkerson的思想是,繼續注入新的流量,新的流量可以和邊上已經有的流量進行抵消。Ford-Fulkerson思想也稱作“擴充路徑方法”,該方法是很多其它網絡流算法的基礎。下面具體來看:
基本概念:

  • 飽和弧和零流弧:給定可行流f=\{f_{ij}\},網絡中f_{ij}=c_{ij}的弧稱爲飽和弧,f_{ij} \leq c_{ij}的弧稱爲非飽和弧,f_{ij}=0的弧稱爲0流弧。
  • 前向弧與反向弧:若p是連接源點v_s到匯點v_t的一條路徑,稱與p的方向相同的弧爲前向弧;與p方向相反的弧爲反向弧(原圖中並沒有)。前向弧的全體記爲p^+,反向弧的全體記爲p^-
  • 殘量圖:帶前向弧和反向弧的圖稱爲殘量圖。
  • 增廣路徑:若可行流f中的任意前向弧是非飽和弧,任意反向弧是非零流弧,稱f對應的路徑爲增廣路徑。

初始時網絡流大小爲0。如果邊上已佔用流量爲x,則殘量圖中正向邊權值變爲c-x表示還有c-x的流量可以通過;反向邊權值爲x表示可以被抵消的流量爲x,在每次迭代中,Ford-Fulkerson方法通過在殘量圖中尋找一條“增廣路徑”(augument path)來增加流的值,直到沒有增廣路徑此時網絡流量達到最大。其正確性由最大流最小割定理保證(略),Ford-Fulkerson算法迭代停止時得到的流是最大流當且僅當殘量圖中不包含增廣路徑。而尋找增廣路徑的方式可以不同,這對應了不同的算法。

Edmonds-Karp算法

該算法在殘量圖中尋找增廣路徑基於圖的BFS遍歷。通過BFS遍歷不斷在殘量圖中尋找增廣路徑。

算法實現

import java.util.*;

public class MaxFlow {
    private WeightedGraph network;
    private WeightedGraph rG; // 殘量圖
    private int s, t;
    private int maxFlow = 0;
    public MaxFlow(WeightedGraph network, int s, int t){
        if(!network.isDirected())
            throw new IllegalArgumentException("Directed Graph Only!");
        if(network.V() < 2)
            throw new IllegalArgumentException("At least 2 vertexs!");

        network.validateVertex(s);
        network.validateVertex(t);

        if(s == t)
            throw new IllegalArgumentException("Should be different!");

        this.network = network;
        this.s = s;
        this.t = t;

        this.rG = new WeightedGraph(network.V(), true);
        // 遍歷network 創建殘量圖
        for(int v = 0; v < network.V(); v ++)
            for(int w: network.adj(v)){
                int c = network.getWeight(v, w);
                rG.addEdge(v, w, c);
                rG.addEdge(w, v, 0);
            }

        while(true){
            ArrayList<Integer> augPath = getAugumentingPath();
            if(augPath.size() == 0) break;

            int f = 0x3f3f3f3f;
            // 計算增廣路徑上的最小值
            for(int i = 1; i < augPath.size(); i ++){
                int v = augPath.get(i - 1);
                int w = augPath.get(i);
                f = Math.min(rG.getWeight(v, w), f);
            }
            maxFlow += f;
            // 根據增廣路徑更新rG正反向邊的權值
            for(int i = 1; i < augPath.size(); i ++){
                int v = augPath.get(i - 1);
                int w = augPath.get(i);
                rG.setWeight(w, v, rG.getWeight(w, v) + f);
                rG.setWeight(v, w, rG.getWeight(v, w) - f);
            }
        }
    }

    private ArrayList<Integer> getAugumentingPath(){
        Queue<Integer> q = new LinkedList<>();
        int []pre = new int[network.V()];
        // pre 同時兼有visited作用, 初始爲 -1,不爲-1說明訪問過
        Arrays.fill(pre, -1);
        q.add(s);
        pre[s] = s;

        while(!q.isEmpty()){
            int cur = q.remove();
            if(cur == t) break;

            for(int next: rG.adj(cur))
                if(pre[next] == -1 && rG.getWeight(cur, next) > 0){
                    pre[next] = cur;
                    q.add(next);
                }
        }
        ArrayList<Integer> res = new ArrayList<>();
        if(pre[t] == -1)// 沒有其它增廣路徑了
            return res;

        int cur = t;
        while(cur != s){
            res.add(cur);
            cur = pre[cur];
        }
        res.add(cur);
        Collections.reverse(res);
        return res;
    }
    public int result(){
        return maxFlow;
    }
    public int flow(int v, int w){
        // v - w 邊上實際的流量
        if(!network.hasEdge(v, w)) throw new IllegalArgumentException("no v-w");
        return rG.getWeight(w, v);
    }
    public static void main(String args[]){
        WeightedGraph g = new WeightedGraph("wg2.txt", true);
        MaxFlow mf = new MaxFlow(g, 0, 5);
        System.out.println(mf.result());

        for(int v = 0; v < g.V(); v ++)
            for(int w: g.adj(v))
                System.out.println(String.format("%d-%d : %d / %d", v, w, mf.flow(v, w), g.getWeight(v, w)));
    }
}
/* wg2.txt
6 9
0 1 9
0 3 9
1 2 8
1 3 10
2 5 10
3 2 1
3 4 3
4 2 8
4 5 7
*/

一個經典的最大流問題:棒球比賽

問題描述
在一場職業棒球比賽中,每隊要打162場比賽,最終勝利場次最多的隊伍爲冠軍,如果有平局,則進行加賽。比賽過程中,如果發現一個隊伍無論如何都不可能奪冠,則直接淘汰。
下表是棒球比賽目前排名前5的隊伍的比賽情況,問:E隊伍是否有可能奪冠?

隊伍 贏次 輸次 剩餘場次 剩餘比賽
A 75 59 28 B3、C8、D7、E3
B 71 63 28 A3、C2、D7、E4
C 69 66 27 A8、B2
D 63 72 27 A7、B7
E 49 86 27 A3、B4

(問題來源:Princeton University CS Assignment。)
注:B3表示和B還有3場比賽。以此類推。
問題分析與建模
直觀來看E隊伍剩下的27場全勝,則會勝利76場,有可能超過A、B、C、D隊伍,但是由於A、B、C、D隊伍之間也有比賽,這就使得問題沒有那麼簡單。我們要看的就是A、B、C、D隊伍間的比賽後有沒有可能使得每一個隊伍的勝場都不超過76。這就可以轉化爲一個網絡流問題。

黑色的箭頭表示剩下隊伍間存在的比賽場次,綠色的箭頭表示具體各個隊伍勝場次分配(權值等於其前一個黑色邊的權值,如點A-B到A和B的兩邊權值都爲3,表示A和B之間誕生3場勝利在A和B之間分配),紅色表示各個隊伍最多還能勝利多少場次。如果這個網絡的最大流是27,意味着可以分配他們的勝利場次使得每個隊伍最多勝76場,也就是E隊伍仍然有可能奪冠。
問題解答

可以看到該網絡的最大流爲26,故E隊伍無論如何都不可能奪冠。

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