最大流问题和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队伍无论如何都不可能夺冠。

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