玩转编程大挑战系列(24):搞定最大流与最小割的3.5步骤详解
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434644
挑战程序竞赛系列(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∈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(F\vert E\vert),再来看看dinic算法的核心思想。
参考博文:http://blog.****.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的随意访问,限制条件:
满足:levelfrom + 1 == levelto的顶点才能被用来寻找增广路径。
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;
}
}