网络流初步
0. 基本定义
一个网络 \(G=(V,E)\) 是一张有向图,\(G\) 中每条有向边 \((x,y)\in E\) 都有一个给定的权值 \(c(x,y)\),称作边的容量。特别地,若 \((x,y)\not\in E\),则 \(c(x,y)=0\)。\(G\) 中还有两个特殊节点 \(s,t\in V\;(s\ne t)\),分别称为源点和汇点。
函数 \(f(x,y)\) 是定义在节点二元组 \((x\in V, y\in V)\) 上的实数函数,具有三条基本性质:
- 容量限制:\(f(x,y)\le c(x,y)\)。
- 斜对称性:\(f(x,y)=-f(y,x)\)。
- 流量守恒:\(\sum_{(u,x)\in E} f(u,x)=\sum_{(x,v)\in E} f(x,v)\)。
\(f\) 即为 \(G\) 的流函数,对于 \((x,y)\in E\),\(f(x,y)\) 称为边的流量。\(c(x,y)-f(x,y)\) 称为边的剩余容量。\(\sum_{(s,v)\in E} f(s,v)\) 称作整个网络的流量。
1. 最大流
最大流:网络的最大流量。
1.1 EK 增广路算法
增广路:一条从源点到汇点的所有边的剩余容量 \(\ge 0\) 的路径。
残留网:由网络中所有结点和剩余容量大于 \(0\) 的边构成的子图,这里的边
包括有向边和其反向边。
建图时每条有向边 \((x,y)\) 都构建一条反向边 \((y,x)\),初始容量 \(c(y,x)=0\)。
构建反向边的目的是提供一个“退流管道”,一旦前面的增广路堵死可行流可以通过“退流管道”退流,提供了“后悔机制”。
EK 算法通过 BFS 找到增广路并不断更新流量计算最大流。
算法流程:
- BFS 计算出一条从 \(s\rightarrow t\) 的增广路,并更新 \(pre\)。
- 通过增广路更新增广路上的流量限制,并不断累加。
时间复杂度 \(O(nm^2)\)(通常适用于 \(10^3\sim 10^4\) 规模的网络)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 210, M = 10010, INF = 2e9;
int n, m, S, T;
int idx = 1, e[M], h[N], ne[M]; ll c[M];
int pre[N]; ll f[M]; //pre[u] 表示u的前驱边
void add(int u, int v, int w) {
e[++ idx] = v, ne[idx] = h[u], c[idx] = w, h[u] = idx;
}
bool bfs() { // 求增广路
memset(f, 0, sizeof f), memset(pre, 0, sizeof pre), f[S] = INF;
queue<int> q; q.push(S);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (!f[v] && c[i] > 0) {
f[v] = min(f[u], c[i]), pre[v] = i;
q.push(v);
if (v == T) return 1;
}
}
}
return 0;
}
ll EK() {
ll flow = 0;
while (bfs()) {
int v = T;
while (v != S) {
int i = pre[v];
c[i] -= f[T], c[i^1] += f[T], v = e[i^1];
}
flow += (ll)f[T];
}
return flow;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d%d%d", &n, &m, &S, &T);
int u, v, w;
for (int i = 1; i <= m; ++i) {
scanf("%d%d%d", &u, &v, &w);
add(u, v, w), add(v, u, 0); // 建一条权值为0的反向边,可以反悔
}
ll flow = EK();
printf("%lld\n", flow);
return 0;
}
1.2 Dinic 算法
注意到 EK 算法中一次 BFS 只能计算一条增广路,而 Dinic 算法一次可以更新多条增广路。
令 \(d_u\) 表示节点 \(u\) 的层次,其表示 \(s\rightarrow u\) 最少需要经过的边数。在残量网络中,满足 \(d_v=d_u+1\) 的边 \((x,y)\) 构成的子图被称为分层图,其显然是一张有向无环图。
算法流程:
- BFS 对点分层,找出增广路;
- DFS 多路增广:
- 搜索顺序优化(分层限制)
- 当前弧优化
- 剩余流量优化
- 残枝优化
- Dinic 累加可行流。
点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 210, M = 10010, INF = 2e9;
int n, m, S, T;
int idx = 1, e[M], ne[M], h[N]; ll c[M];
int d[N], cur[N]; // 当前弧
void add(int u, int v, int w) {
e[++ idx] = v, ne[idx] = h[u], c[idx] = w, h[u] = idx;
}
bool bfs() {
memset(d, 0, sizeof d);
queue<int> q; q.push(S), d[S] = 1, cur[S] = h[S];
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (!d[v] && c[i] > 0) {
d[v] = d[u] + 1, q.push(v), cur[v] = h[v];
if (v == T) return 1;
}
}
}
return 0;
}
ll dfs(int u, ll mf) {
if (u == T) return mf;
ll sum = 0;
for (int i = cur[u]; i != -1; i = ne[i]) {
cur[u] = i; // 当前弧优化
int v = e[i];
if (d[v] == d[u]+1 && c[i] > 0) {
ll f = dfs(v, min(mf, c[i]));
c[i] -= f, c[i^1] += f, sum += f, mf -= f;
if (!mf) break; // 剩余流量优化
}
}
if (!sum) d[u] = 0; // 残枝优化
return sum;
}
ll dinic() {
ll flow = 0;
while (bfs()) flow += dfs(S, INF);
return flow;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d%d%d", &n, &m, &S, &T);
for (int i = 1; i <= m; ++i) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
add(u, v, w), add(v, u, 0);
}
ll flow = dinic();
printf("%lld\n", flow);
return 0;
}
2. 最小割
若一个边集 \(E'\subseteq E\) 被删去后,源点 \(s\) 和汇点 \(t\) 不再联通,则称该边集为网络的割。记 \(s\) 所在点的集合为 \(S\),\(t\) 所在点的集合为 \(T\),则割 \((S,T)\) 的容量 \(c(S,T)\) 表示所有从 \(S\) 到 \(T\) 的边的容量之和。割的容量之和最小的割称为网络的最小割。
最大流最小割定理:任何一个网络 \(G\) 的最大流等于最小割中边的容量之和。
证明:
假设最小割 \(<\) 最大流,那么割去这些边后,因为网络流量尚未最大化,所以仍然可以找到一条 \(S\rightarrow T\) 的增广路,矛盾。所以最小割 $\ge $ 最大流。
如果我们求出了最大流 \(f\),那么此时一定不存在 \(s\) 到 \(t\) 的增广路,即 \(S\) 的出边一定是满流,\(S\) 的入边一定是零流。那么有:\(f(s,t)=\sum f_{out}(S)-\sum f_{in}(S)=\sum f_{out}(S)=c(S,T)\)。得证。
考虑如何计算最小割的最小边数,有两种方法:
- 将边权改为 \((m+1)c_i+1\),此时由于最小割边数不可能超过 \(m\),所以此时最小割 \(\bmod m\) 即为最小割的最小边数。
- 将满流的边设为 \(1\),未满流的边设为 \(+\infty\),此时的最小割即为原最小割的最小边数。
P1344 [USACO4.4] 追查坏牛奶 Pollutant Control
点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 35, M = 2e3+10, INF = 2e9;
int n, m;
int idx = 1, e[M], ne[M], h[N], c[M];
int pre[N], f[N];
void add(int u, int v, int w) {
e[++ idx] = v, ne[idx] = h[u], c[idx] = w, h[u] = idx;
}
bool bfs() {
memset(f, 0, sizeof f), memset(pre, 0, sizeof pre), f[1] = INF;
queue<int> q; q.push(1);
while (q.size()) {
int u = q.front(); q.pop();
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (!f[v] && c[i]) {
f[v] = min(f[u], c[i]), pre[v] = i;
q.push(v);
if (v == n) return 1;
}
}
}
return 0;
}
ll EK() {
ll flow = 0;
while (bfs()) {
int v = n;
while (v != 1) {
int i = pre[v];
c[i] -= f[n], c[i^1] += f[n], v = e[i^1];
}
flow += (ll)f[n];
}
return flow;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
add(u, v, w), add(v, u, 0);
}
ll max_flow = EK();
printf("%lld ", max_flow);
for (int i = 2; i <= m*2; i += 2) {
if (!c[i]) c[i] = 1; else c[i] = INF;
c[i^1] = 0;
}
ll min_cut = EK();
printf("%lld ", min_cut);
return 0;
}

浙公网安备 33010602011771号