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 加入点集,因为最小割的边的容量一定是满的。
这题一开始理解错了,还以为是求割边的点集合。
代码
Status | Accepted |
---|---|
Time | 32ms |
Memory | 4096kB |
Length | 2521 |
- #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 星期日