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

来源:互联网 发布:mac口红dangerous 编辑:程序博客网 时间:2024/06/07 02:56

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

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

练习题如下:

  • POJ 1273: Drainage Ditches
  • POJ 3713: Transferring Sylla

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,sS,tT,说明s属于集合S,t属于集合T,这样源点和汇点分属两个不同集合,有什么好处呢?

定理:

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

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

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

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

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

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

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

《算法导论》P423告诉我们,当不存在增广路径时,存在一个最小割集,使得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|条,所以大致估算时间复杂度为O(F|E|),再来看看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;    }}
阅读全文
0 0