挑战程序竞赛系列(24):3.5最大流与最小割

时间:2022-06-22
本文章向大家介绍挑战程序竞赛系列(24):3.5最大流与最小割,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014688145/article/details/75507959

挑战程序竞赛系列(24):3.5最大流与最小割

详细代码可以fork下Github上leetcode项目,不定期更新。

练习题如下:

POJ 1273: Drainage Ditches

最大流(经典问题),思路:

这类题类似于松弛法,不需要一步给出答案,而是慢慢的逼近最优解,何以理解?

首先给出一个最大流问题,问的是从源点到汇点所能允许的最大流量是多少?

在没接触最大流之前,我的一个想法是,从汇点开始,搜集所有进入汇点的边,看是否能够满足最大容量,显然并不是所有的边都能以最大流量流入汇点,这就意味着对于中间结点还需要针对所有边判断一遍,无形之中增添了麻烦。

尝试贪心(核心想法:解是在不断改进中,直到无法改进)

既然是最大化流,就找一条从s到t的路径,记录路径中最小的容量(瓶颈),能够找到这样的s到t的路径,就让当前flow加上此流量,直到没有路径抵到。当边满容量时,边可以看作失效。

这是最接近答案的想法,但上述策略是错误的,《挑战》上P210有经典的反例,策略为什么会错?

并没有最大化每一条可能路径,边太容易失效了,这的确不太直观,或许也很难想到反例。不多说,再看《挑战》P211上的最优解,和次优解比较下,就发现一些端倪。有一条差值为1和-1的路径,这是为何?

-1表示可以把流量推回去,其实是把原先从s->1那里来的流量,从2->t中减去1,接着把s->2的流量加上1,这样,2->t的流量看上去没有变化,那么减去的跑到哪去了?跑到路径1->3->t上了,这样相当于让原先的流量转了个弯,的确高明。

简单来说,增广路径可以让原先跑错的流量反悔!推回到它该去的地方,如果不存在增广路径,则说明已经最优了。

证明的思路很简单,反证法,需要注意两点:

  • 对于每一条增广路径,流量f会在原来的基础上加上增广路径的流量,处于不断递增的状态,不会出现递减。
  • 所以在此基础上,当不存在增广路径时,流量就不会再递增, 自然达到了最大值(反证法)

给我的启示:

一类问题不需要直接得出答案,可以找寻一个性质慢慢逼近答案,这性质和答案成单调关系,那么当不存在该性质时,自然达到了最优。

具体证明可以参考《算法导论》P420页,写的很详细,仔细推很容易得到答案。

最小割集和最大流的对偶性证明:

抓住割集的定义即可,首先,任何有s和t的有向图,存在集合S和集合T,s∈S,t∈Ts in S, t in T,说明s属于集合S,t属于集合T,这样源点和汇点分属两个不同集合,有什么好处呢?

定理:

对于给定的流f,横跨任何切割的净流量都相同,这就意味我们可以对S和T进行任意切分,集合S可以等价于s,集合T可以等价于t,或许我们能找到割集容量和最大流的关系?

证明:利用流的守恒性质,简单说说,因为源点没有入边,它属于能源始发地,但连接源点的结点符合流守恒性质,所以我们完全可以把流量F扩散到切割的的边界结点上,看是否符合f(S,T)f(S,T)的定义。数学上就利用源点F的公式和流量守恒开始构造。

最小割集:min{c(S,T)}

既然可以任意切分割集,那就意味着不同割集的容量不同,由流量限制可以得:

f(S,T)≤c(S,T)

f(S, T) le c(S, T) 所以f(S,T)最大只有c(S,T)c(S, T),显然要取最小割集,才能整体符合流量限制,所以有:

f(S,T)≤min{c(S,T)}

f(S,T) le min {c(S, T)}

所以说f(S,T)最大也就最小割集那么大了,那到底是比最小割集小呢还是最大流正好等于最小割集呢?

《算法导论》P423告诉我们,当不存在增广路径时,存在一个最小割集,使得f(S,T)=c(S,T)f(S, T) = c(S, T),即最小割集就是最大流。

所以说:求最大流就等于求最小割集,这两个问题无形当中等价了。

开始代码吧:

public class SolutionDay04_P1273 {

    static class Edge{
        int from;
        int to;
        int cap;

        public Edge(int from, int to, int cap){
            this.from = from;
            this.to = to;
            this.cap = cap;
        }

        @Override
        public String toString() {
            return "Edge [from=" + from + ", to=" + to + ", cap=" + cap + "]";
        }
    }



    static List<Edge>[] g;
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNext()){
            String line = in.nextLine().trim();
            String[] nums = line.split(" ");
            int M = Integer.parseInt(nums[0]);
            int N = Integer.parseInt(nums[1]);
            g = new ArrayList[N];
            for (int i = 0; i < N; ++i){ 
                g[i] = new ArrayList<Edge>();
                for (int j = 0; j < N; ++j){
                    g[i].add(new Edge(i, j, 0));
                }
            }
            for (int i = 0; i < M; ++i){
                line = in.nextLine().trim();
                nums = line.split(" ");
                int from = Integer.parseInt(nums[0]);
                int to = Integer.parseInt(nums[1]);
                int cap = Integer.parseInt(nums[2]);
                from--;
                to--;
                g[from].add(new Edge(from, to, cap));
            }
            System.out.println(fordFulkerson());
        }
        in.close();
    }


    static final int INF = 1 << 30;
    public static int fordFulkerson(){
        int n = g.length;
        int flow = 0;
        for (;;){
            boolean[] visited = new boolean[n];
            int d = dfs(0, n - 1, INF, visited);
            if (d != 0){
                flow += d;
            }
            else break;
        }
        return flow;
    }

    public static int dfs(int s, int t, int F, boolean[] visited){
        if (s == t) return F;
        visited[s] = true;
        for (Edge e : g[s]){
            int to = e.to;
            if (!visited[to] && e.cap > 0){
                int d = dfs(to, t, Math.min(e.cap, F), visited);
                if (d > 0){
                    e.cap -= d;
                    g[to].get(s).cap += d;
                    return d;
                }
            }
        }
        return 0;
    }

}

说说DFS的细节吧,跟着遍历路径到汇点t,不断更新最小的流,接着在自底向上回归的时候构造残余网络,所以需要传入一个F。

POJ 3713: Transferring Sylla

好吧,此题的解法跟强连通分量有关,《挑战》把它归到最大流一定有它的理由,但本人道行还潜,暂且用其他方法吧。既然刷到了,就顺便学习下两种强连通分量算法,Kosaraju算法和Tarjan算法。

Kosaraju算法求强连通:(hdu: 1269)

public class SolutionDay09_H1269 {
    Scanner is;
    PrintWriter out;

    class Edge{
        int from;
        int to;

        public Edge(int from, int to){
            this.from = from;
            this.to = to;
        }
    }

    List<Edge>[] g;
    List<Edge>[] rg;
    boolean[] marked;
    void solve() {
        while (is.hasNext()){
            int N = is.nextInt();
            int M = is.nextInt();
            if (N + M == 0) break;
            g = new ArrayList[N];
            rg = new ArrayList[N];
            for (int i = 0; i < N; ++i){
                g[i] = new ArrayList<>();
                rg[i] = new ArrayList<>();
            }

            for (int i = 0; i < M; ++i){
                int from = is.nextInt();
                int to = is.nextInt();
                from--;
                to--;
                g[from].add(new Edge(from, to));
                rg[to].add(new Edge(to, from));
            }
            marked = new boolean[N];    
            DepthFirstOrder order = new DepthFirstOrder(rg);
            Stack<Integer> reverseOrder = order.reverseOrder();
            int cnt = 0;
            while (!reverseOrder.isEmpty()){
                int v = reverseOrder.pop();
                if (!marked[v]){
                    dfs(g, v);
                    cnt++;
                }
            }
            if (cnt == 1) out.println("Yes");
            else out.println("No");
        }
    }

    private void dfs(List<Edge>[] g, int v){
        marked[v] = true;
        for (Edge e : g[v]){
            int to = e.to;
            if (!marked[to]) dfs(g, to);
        }
    }

    class DepthFirstOrder{

        boolean[] marked;
        Stack<Integer> reverse;
        List<Edge>[] graph;

        public DepthFirstOrder(List<Edge>[] graph){
            this.graph = graph;
            int n = graph.length;
            marked = new boolean[n];
            reverse = new Stack<>();
            for (int i = 0; i < n; ++i){
                if (!marked[i]) dfs(graph, i);
            }
        }

        public void dfs(List<Edge>[] g, int v){
            marked[v] = true;
            for (Edge e : g[v]){
                int to = e.to;
                if (!marked[to]) dfs(g, to);
            }
            reverse.push(v);
        }

        public Stack<Integer> reverseOrder(){
            return this.reverse;
        }
    }

    void run() throws Exception {
        is = new Scanner(System.in);
        out = new PrintWriter(System.out);
        solve();
        out.flush();
    }

    public static void main(String[] args) throws Exception {
        new SolutionDay09_H1269().run();
    }
}

对逆序图求reversePostOrder,可以想象一辆taxi,开往各种路径,而强连通分量可以让taxi返回原点(虫洞),检测虫洞是这些算法的关键。

Tarjan算法:

static class Edge{
        int from;
        int to;
        public Edge(int from, int to){
            this.from = from;
            this.to = to;
        }
    }

    static List<Edge>[] g;
    static int[] low;
    static int[] dfn;
    static int[] belong;
    static int index;
    static Stack<Integer> stack;
    static boolean[] instack;
    static int sum;

    public static void tarjan(int s){
        int j;
        dfn[s] = low[s] = ++index;
        instack[s] = true;
        stack.push(s);
        for (Edge e : g[s]){
            j = e.to;
            if (dfn[j] == 0){
                tarjan(j);
                if (low[j] < low[s]) low[s] = low[j];
            }
            else if (instack[j] && dfn[j] < low[s]){ //无环情况下,走不到这,有环会进入该循环。
                low[s] = dfn[j]; //找到了虫洞,dfn在之前已经被访问过
            }
        }

        if (dfn[s] == low[s]){
            sum ++;
            do{
                j = stack.pop();
                instack[j] = false;
                belong[j] = sum;
            }
            while (j != s);
        }
    }

    public static void main(String[] args) throws Exception {
        Scanner in = new Scanner(System.in);
        while (in.hasNext()){
            int N = in.nextInt();
            int M = in.nextInt();
            if (N == 0 && M == 0) continue;

            g = new ArrayList[N];

            for (int i = 0; i < N; ++i) g[i] = new ArrayList<>();
            for (int i = 0; i < M; ++i){
                int from = in.nextInt();
                int to = in.nextInt();
                from --;
                to --;
                g[from].add(new Edge(from, to));
            }

            low = new int[N];
            dfn = new int[N];
            belong = new int[N];
            index = 0;
            stack = new Stack<>();
            instack = new boolean[N];
            sum = 0;

            for (int i = 0; i < N; ++i){
                if (dfn[i] == 0){
                    tarjan(i);
                }
            }
            if (sum == 1) System.out.println("Yes");
            else System.out.println("No");
        }
        in.close();
    }

dfn是访问结点的timeStamp,当存在环时,会更新low(时光倒流),当出现时空与当前stamp不一致时,可以认为它们同属于一个强连通中,我们的目标是把所有的强连通分量找出来。

此题未AC,具体可以参考:http://www.hankcs.com/program/algorithm/poj-3713-transferring-sylla.html

我的代码:(TLE)

    static class Edge{
        int from;
        int to;

        public Edge(int from, int to){
            this.from = from;
            this.to = to;
        }
    }

    static List<Edge>[] g;
    public static void main(String[] args) throws IOException {
        Scanner in = new Scanner(System.in);
        while (in.hasNext()){
            int N = in.nextInt();
            int M = in.nextInt();
            if (N == 0 && M == 0) break;

            init(N);

            g = new ArrayList[N];
            for (int i = 0; i < N; ++i) g[i] = new ArrayList<Edge>();

            for (int i = 0; i < M; ++i){
                int from = in.nextInt();
                int to = in.nextInt();
                g[from].add(new Edge(from, to));
                g[to].add(new Edge(to, from));
            }
            System.out.println(solve() ? "YES" : "NO");
        }
        in.close();
    }

    static int V;
    static int[] dfn;
    static int[] low;
    static int index;
    static int[] status; // 0. 没有访问 1. 正在访问 2. 已经访问
    static int root;
    static int[] is_cut_vertex;
    static boolean has_cut_vertex;
    private static void init(int N){
        V = N;
        dfn = new int[N];
        low = new int[N];
        index = 0;
        status = new int[N];
        is_cut_vertex = new int[N];
        has_cut_vertex = false;
    }

    public static void tarjan(int x, int from){
        status[x] = 1;
        dfn[x] = low[x] = ++index;
        int sub_tree = 0;
        int v;
        for (Edge e : g[x]){
            v = e.to;
            if (v != from && status[v] == 1){
                low[x] = Math.min(low[x], dfn[v]);
            }
            if (status[v] == 0){
                tarjan(v, x);
                ++sub_tree;
                low[x] = Math.min(low[x], low[v]);
                if ((x == root && sub_tree > 1) || x != root && low[v] >= dfn[x]){
                    is_cut_vertex[x] = 1;
                    has_cut_vertex = true;
                }
            }
        }
        status[x] = 2;
    }

    private static void calc(int del){
        is_cut_vertex = new int[V];
        status = new int[V];
        low = new int[V];
        dfn = new int[V];

        status[del] = 2;
        root = 0;
        if (del == 0){
            root = 1;
        }
        tarjan(root, -1);
    }

    private static boolean solve(){
        for (int i = 0; i < V; ++i){
            calc(i);
            for (int j = 0; j < V; ++j){
                if (0 == status[j]){
                    has_cut_vertex = true;
                    break;
                }
            }

            if (has_cut_vertex){
                break;
            }
        }
        return !has_cut_vertex;
    }

网络流还有另一种算法Dinic算法,简单说说,参考题目还是POJ 1273: Drainage Ditches

先来分析下ford-fulkerson的时间复杂度,首先假设最大值为flow,可以从循环中看出:

    for (;;){
            boolean[] visited = new boolean[n];
            int d = dfs(0, n - 1, INF, visited);
            if (d != 0){
                flow += d;
            }
            else break;
        }

所以dfs最坏情况下需要flow次(假设每次增广路径增加的流量为1),而dfs在最坏情况下搜索的|E|vert E vert条,所以大致估算时间复杂度为O(F|E|)O(Fvert Evert),再来看看dinic算法的核心思想。

参考博文:http://blog.csdn.net/wall_f/article/details/8207595

将残留网络中所有的顶点的层次标注出来的过程称为分层。

注意:

  1. 对残留网路进行分层后,弧可能有3种可能的情况。
    • 从第i层顶点指向第i+1层顶点。
    • 从第i层顶点指向第i层顶点。
    • 从第i层顶点指向第j层顶点(j < i)。
  2. 不存在从第i层顶点指向第i+k层顶点的弧(k>=2)。
  3. 并非所有的网络都能分层。

上述第2条可以用反证法,假设存在第i层的顶点指向第i+k层顶点的弧,那么有bfs对距离的定义,第i+k层的那个顶点必然会出现在第i+1层中,与假设矛盾。

有了这些性质,我们便可以利用BFS构造分层图了,它每次寻找增广路径时,都选取最短路径下的增广路径,也就是说限制了dfs的随意访问,限制条件:

满足:level[from] + 1 == level[to]的顶点才能被用来寻找增广路径。

why?为了当前弧的优化,依然使用反证法,假设把流量送到第i+1层后的某个顶点,且存在第j层的顶点(j < = i + 1),再把流量送回到第j层,且从第j层能找到去汇点t的增广路径,那么该增广路径,完全可以直接从第j层去汇点t,而不需要多此一举,经过第i+1层再到汇点t。

或许分层当前弧优化还有更直观的解释,暂且就以这种方式记忆吧。

代码如下:

public class SolutionDay20_P1273 {

    static class Edge{
        int from;
        int to;
        int cap;

        public Edge(int from, int to, int cap){
            this.from = from;
            this.to = to;
            this.cap = cap;
        }

        @Override
        public String toString() {
            return from + " " + to + " " + cap;
        }
    }


    static List<Edge>[] g;
    static int[] level;
    static int N;
    static int INF = 1 << 30;
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNext()){
            String[] nums = in.nextLine().trim().split(" ");
            int M = Integer.parseInt(nums[0]);
            N = Integer.parseInt(nums[1]);
            level = new int[N];

            g = new ArrayList[N];
            for (int i = 0; i < N; ++i){ 
                g[i] = new ArrayList<Edge>();
                for (int j = 0; j < N; ++j){
                    g[i].add(new Edge(i, j, 0));
                }
            }

            for (int i = 0; i < M; ++i){
                nums = in.nextLine().trim().split(" ");
                int from = Integer.parseInt(nums[0]);
                int to = Integer.parseInt(nums[1]);
                int cap = Integer.parseInt(nums[2]);
                from --;
                to --;
                addEdge(from, to, cap);
            }
            System.out.println(dinic());
        }
        in.close();
    }

    public static void addEdge(int from, int to, int cap){
        g[from].add(new Edge(from, to, cap));
    }

    public static void bfs(int s){
        for (int i = 0; i < N; ++i) level[i] = -1;
        level[s] = 0;
        Queue<Integer> queue = new LinkedList<Integer>();
        queue.offer(s);
        while (!queue.isEmpty()){
            int v = queue.poll();
            for (Edge e : g[v]){
                int to = e.to;
                if (e.cap > 0 && level[to] < 0){
                    level[to] = level[v] + 1;
                    queue.offer(to);
                }
            }
        }
    }

    public static int dfs(int s, int t, int f, boolean[] visited){
        if (s == t) return f;
        visited[s] = true;
        for (Edge e : g[s]){
            int from = e.from;
            int to = e.to;
            if (!visited[to] && level[from] + 1 == level[to] && e.cap > 0){
                int d = dfs(to, t, Math.min(f, e.cap), visited);
                if (d > 0){
                    e.cap -= d;
                    g[to].get(s).cap += d;
                    return d;
                }
            }
        }
        return 0;
    }


    public static int dinic(){
        int flow = 0;
        for(;;){
            bfs(0);
            if (level[N - 1] < 0) break;
            int f = 0;
            while ((f = dfs(0, N - 1, INF, new boolean[N])) > 0) flow += f;
        }
        return flow;
    }

}