挑战程序竞赛系列(24):3.5最大流与最小割
2017-07-20 12:41
387 查看
挑战程序竞赛系列(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,s∈S,t∈T,说明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 ; 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 ; 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 ; rg = new ArrayList ; 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 ; 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 ; 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 ; 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 ; dfn = new int ; belong = new int ; index = 0; stack = new Stack<>(); instack = new boolean ; 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 ; 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 ; low = new int ; index = 0; status = new int ; is_cut_vertex = new int ; 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 ; 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
将残留网络中所有的顶点的层次标注出来的过程称为分层。
注意:
对残留网路进行分层后,弧可能有3种可能的情况。
从第i层顶点指向第i+1层顶点。
从第i层顶点指向第i层顶点。
从第i层顶点指向第j层顶点(j < i)。
不存在从第i层顶点指向第i+k层顶点的弧(k>=2)。
并非所有的网络都能分层。
上述第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 ; g = new ArrayList ; 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 )) > 0) flow += f; } return flow; } }
相关文章推荐
- 挑战程序竞赛系列(25):3.5最大权闭合图
- 挑战程序竞赛系列(26):3.5二分图匹配(1)
- 挑战程序竞赛系列(66):4.7字符串匹配(1)
- 挑战程序竞赛系列(5):2.1广度优先搜索
- 挑战程序竞赛系列(41):4.1中国剩余定理
- 挑战程序竞赛系列(69):4.7后缀数组(1)
- 挑战程序竞赛系列(80):4.3 2-SAT(4)
- 挑战程序竞赛系列(16):3.1最大化最小值
- 挑战程序竞赛系列(90):3.6凸包(1)
- 挑战程序竞赛系列(23):3.2折半枚举
- 挑战程序竞赛系列(43):4.1矩阵 高斯消元
- 挑战程序竞赛系列(71):4.7高度数组(1)
- 挑战程序竞赛系列(76):4.3强连通分量分解(3)
- 挑战程序竞赛系列(49):4.2 推理与动态规划算法(2)
- 挑战程序竞赛系列(85):3.6极限情况(2)
- 挑战程序竞赛系列(17):3.1最大化平均值
- 挑战程序竞赛系列(36):3.3线段树和平方分割
- 挑战程序竞赛系列(75):4.3强连通分量分解(2)
- 挑战程序竞赛系列(64):4.7字符串上的动态规划(2)
- 挑战程序竞赛系列(93):3.6凸包(4)