网络流初步

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)\) 上的实数函数,具有三条基本性质:

  1. 容量限制:\(f(x,y)\le c(x,y)\)
  2. 斜对称性:\(f(x,y)=-f(y,x)\)
  3. 流量守恒:\(\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 找到增广路并不断更新流量计算最大流。

算法流程:

  1. BFS 计算出一条从 \(s\rightarrow t\) 的增广路,并更新 \(pre\)
  2. 通过增广路更新增广路上的流量限制,并不断累加。

时间复杂度 \(O(nm^2)\)(通常适用于 \(10^3\sim 10^4\) 规模的网络)。

P3376 【模板】网络最大流

点击查看代码
#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)\) 构成的子图被称为分层图,其显然是一张有向无环图。

算法流程:

  1. BFS 对点分层,找出增广路;
  2. DFS 多路增广:
  • 搜索顺序优化(分层限制)
  • 当前弧优化
  • 剩余流量优化
  • 残枝优化
  1. 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)\)。得证。

考虑如何计算最小割的最小边数,有两种方法:

  1. 将边权改为 \((m+1)c_i+1\),此时由于最小割边数不可能超过 \(m\),所以此时最小割 \(\bmod m\) 即为最小割的最小边数。
  2. 将满流的边设为 \(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;
}
posted @ 2023-07-07 00:36  Jasper08  阅读(23)  评论(0)    收藏  举报