您的位置:首页 > 其它

挑战程序竞赛系列(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;
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: