MENU

Hihocoder1378 网络流二・最大流最小割定理(最大流 + 最小割)

2018 年 07 月 22 日・阅读: 3101・图论阅读设置

1378 : 网络流二・最大流最小割定理

描述

小 Hi:在上一周的 Hiho 一下中我们初步讲解了网络流的概念以及常规解法,小 Ho 你还记得内容么?

小 Ho:我记得!网络流就是给定了一张图 G=(V,E),以及源点 s 和汇点 t。每一条边 e (u,v) 具有容量 c (u,v)。网络流的最大流问题求解的就是从 s 到 t 最多能有多少流量。

小 Hi:那这个问题解决办法呢?

小 Ho:解决网络流的基本思路就是寻找增广路,不断更新残留网络。直到找不到新的增广路,此时得到的流就是该网络的最大流。

小 Hi:没错,看来你记得很牢嘛。

小 Ho:哎嘿嘿,不过这里我有一个问题,为什么找不到增广路时就已经找到了最大流呢?

小 Hi:这一次我就来解决你的疑惑,首先我们要从网络流的割开始讲起。

对于一个网络流图 G=(V,E),其割的定义为一种点的划分方式:将所有的点划分为 S 和 T=V-S 两个部分,其中源点 s∈S,汇点 t∈T。

对于一个割 (S,T),我们定义净流 f (S,T) 表示穿过割 (S,T) 的流量之和,即:

  • f(S,T) = Σf(u,v) | u∈S,v∈T

举个例子 (该例子选自算法导论):

  • 净流f = f(2,4)+f(3,4)+f(3,5) = 12+(-4)+11 = 19

同时我们定义割的容量 C (S,T) 为所有从 S 到 T 的边容量之和,即:

  • C(S,T) = Σc(u,v) | u∈S,v∈T

同样在上面的例子中,其割的容量为:

  • c(2,4)+c(3,5)=12+14=26

小 Ho:也就是说在计算割 (S,T) 的净流 f (S,T) 时可能存在反向的流使得 f (u,v)<0,而容量 C (S,T) 一定是非负数。

小 Hi:你这么说也没错。实际上对于任意一个割的净流 f (S,T) 总是和网络流的流量 f 相等。比如上面例子中我们改变一下割的方式:

可以计算出对于这两种情况净流 f (S,T) 仍然等于 19。

一个直观的解释是:根据网络流的定义,只有源点 s 会产生流量,汇点 t 会接收流量。因此任意非 s 和 t 的点 u,其净流量一定为 0,也即是 Σ(f (u,v))=0。而源点 s 的流量最终都会通过割 (S,T) 的边到达汇点 t,所以网络流的流 f 等于割的静流 f (S,T)。

严格的证明如下:

  • f(S,T) = f(S,V) - f(S,S)
  • 从S到T的流等于从S到所有节点的流减去从S到S内部节点的流
  • f(S,T) = f(S,V)
  • 由于S内部的节点之间存在的流一定有对应的反向流,因此f(S,S)=0
  • f(S,T) = f(s,V) + f(S-s,V)
  • 再将S集合分成源点s和其他属于S的节点
  • f(S,T) = f(s,V)
  • 由于除了源点s以外其他节点不会产生流,因此f(S-s,V)=0
  • f(S,T) = f(s,V) = f

所以 f (S,T) 等于从源点 s 出来的流,也就是网络的流 f。

小 Ho:简单理解的话,也就是说任意一个割的净流 f (S,T) 都等于当前网络的流量 f

小 Hi:是这样的。而对于任意一个割的净流 f (S,T) 一定是小于等于割的容量 C (S,T)。那也即是,对于网络的任意一个流 f 一定是小于等于任意一个割的容量 C (S,T)。

而在所有可能的割中,存在一个容量最小的割,我们称其为最小割

这个最小割限制了一个网络的流 f 上界,所以有:

对于任一个网络流图来说,其最大流一定是小于等于最小割的。

小 Ho:但是这和增广路又有什么关系呢?

小 Hi:接下来就是重点了。利用上面讲的知识,我们可以推出一个最大流最小割定理

  • 对于一个网络流图G=(V,E),其中有源点s和汇点t,那么下面三个条件是等价的:
  • 1. 流f是图G的最大流
  • 2. 残留网络Gf不存在增广路
  • 3. 对于G的某一个割(S,T),此时f = C(S,T)

首先证明 1 => 2

  • 我们利用反证法,假设流f是图G的最大流,但是残留网络中还存在有增广路p,其流量为fp。则我们有流f'=f+fp>f。这与f是最大流产生矛盾。

接着证明 2 => 3

  • 假设残留网络Gf不存在增广路,所以在残留网络Gf中不存在路径从s到达t。我们定义S集合为:当前残留网络中s能够到达的点。同时定义T=V-S
  • 此时(S,T)构成一个割(S,T)。且对于任意的u∈S,v∈T,有f(u,v)=c(u,v)。若f(u,v)<c(u,v),则有Gf(u,v)>0,s可以到达v,与v属于T矛盾。
  • 因此有f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T)。

最后证明 3 => 1

  • 由于f的上界为最小割,当f到达割的容量时,显然就已经到达最大值,因此f为最大流。

这样就说明了为什么找不到增广路时,所求得的一定是最大流。

小 Ho:原来是这样,我明白了。

输入

第 1 行:2 个正整数 N,M。2≤N≤500,1≤M≤20,000。

第 2..M+1 行:每行 3 个整数 u,v,c (u,v),表示一条边 (u,v) 及其容量 c (u,v)。1≤u,v≤N,0≤c (u,v)≤100。

给定的图中默认源点为 1,汇点为 N。可能有重复的边。

输出

第 1 行:2 个整数 A B,A 表示最小割的容量,B 表示给定图 G 最小割 S 集合的点数。

第 2 行:B 个空格隔开的整数,表示 S 集合的点编号。

若存在多个最小割可以输出任意一个的解。

样例输入

  • 6 7
  • 1 2 3
  • 1 3 5
  • 2 4 1
  • 3 4 2
  • 3 5 3
  • 4 6 4
  • 5 6 2

样例输出

  • 5 4
  • 1 2 3 5

链接

https://hihocoder.com/problemset/problem/1378

题意

求最小割,以及最小割中属于 S 部分的点集。

题解

先跑一遍最大流得到最小割,最后再 bfs 一遍,如果搜到有边的残余容量大于 0 加入点集,因为最小割的边的容量一定是满的。

这题一开始理解错了,还以为是求割边的点集合。

代码

StatusAccepted
Time32ms
Memory4096kB
Length2521
  • #include <iostream>
  • #include <cstdio>
  • #include <cstring>
  • #include <algorithm>
  • #include <queue>
  • #include <vector>
  • #include <set>
  • using namespace std;
  • const int maxn = 510;
  • int n, m;
  • struct Edge
  • {
  • int u, v, cap;
  • Edge() {}
  • Edge(int u, int v, int cap): u(u), v(v), cap(cap) {}
  • }edge[maxn*maxn];
  • int R, S, T;
  • vector <int> E[maxn]; //边集
  • int dis[maxn];
  • int current[maxn];
  • vector <int> V;
  • bool flag;
  • void addedge(int u, int v, int cap)
  • {
  • E[u].push_back(R);
  • edge[R++] = Edge(u, v, cap); // 正向边
  • E[v].push_back(R);
  • edge[R++] = Edge(v, u, 0); // 方向边容量为0
  • // 正向边下标通过异或就得到反向边下标, 2 ^ 1 == 3 ; 3 ^ 1 == 2,R从0开始
  • }
  • int bfs()
  • {
  • memset(dis, 0x3f, sizeof(dis)); // 初始化为0x3f3f3f3f
  • queue <int> Q;
  • Q.push(S);
  • dis[S] = 0;
  • if (flag)
  • V.push_back(S);
  • while (!Q.empty())
  • {
  • int u = Q.front();
  • Q.pop();
  • for (int i = 0; i < E[u].size(); ++i)
  • {
  • Edge &e = edge[E[u][i]];
  • if (e.cap > 0 && dis[e.v] == 0x3f3f3f3f)
  • {
  • dis[e.v] = dis[u] + 1;
  • Q.push(e.v);
  • if (flag)
  • V.push_back(e.v);
  • }
  • }
  • }
  • return dis[T] < 0x3f3f3f3f; // 返回是否能够到达汇点
  • }
  • int dinic(int u, int maxflow)
  • {
  • if (u == T)
  • return maxflow;
  • for (int i = current[u]; i < E[u].size(); ++i) // 当前弧优化,保存u点增广到了第几条边
  • {
  • current[u] = i;
  • Edge &e = edge[E[u][i]];
  • if (dis[e.v] == dis[u] + 1 && e.cap > 0)
  • {
  • int flow = dinic(e.v, min(maxflow, e.cap));
  • if (flow)
  • {
  • e.cap -= flow; // 正向边流量减少
  • edge[E[u][i] ^ 1].cap += flow; // 方向边流量增加
  • return flow;
  • }
  • }
  • }
  • return 0;
  • }
  • int DINIC()
  • {
  • int ans = 0;
  • while (bfs())
  • {
  • int flow;
  • memset(current, 0, sizeof(current)); // bfs后清空当前弧数组
  • while (flow = dinic(S, 0x3f3f3f3f)) // 一次bfs可以进行多次增广
  • ans += flow;
  • }
  • return ans;
  • }
  • int main()
  • {
  • int t, u, v, cap;
  • scanf("%d%d", &n, &m);
  • R = 0, S = 1, T = n; // 边序号,源点,汇点
  • for (int i = 0; i <= n; ++i)
  • E[i].clear();
  • V.clear();
  • memset(edge, 0, sizeof(edge));
  • for (int i = 0; i < m; ++i)
  • {
  • scanf("%d%d%d", &u, &v, &cap);
  • addedge(u, v, cap);
  • }
  • int ans = DINIC();
  • flag = true; // 跑完最大流后再bfs一遍得到S点集
  • bfs();
  • printf("%d %d\n", ans, V.size());
  • printf("%d", V[0]);
  • for (int i = 1; i < V.size(); ++i)
  • printf(" %d", V[i]);
  • return 0;
  • }

Dinic 的效率还是比较高的。


The end.
2018-07-22 星期日
最后编辑于: 2018 年 07 月 24 日