网络流初步

0. 基本定义

一个网络 G=(V,E) 是一张有向图,G 中每条有向边 (x,y)E 都有一个给定的权值 c(x,y),称作边的容量。特别地,若 (x,y)E,则 c(x,y)=0G 中还有两个特殊节点 s,tV(st),分别称为源点汇点

函数 f(x,y) 是定义在节点二元组 (xV,yV) 上的实数函数,具有三条基本性质:

  1. 容量限制:f(x,y)c(x,y)
  2. 斜对称性:f(x,y)=f(y,x)
  3. 流量守恒:(u,x)Ef(u,x)=(x,v)Ef(x,v)

f 即为 G流函数,对于 (x,y)Ef(x,y) 称为边的流量c(x,y)f(x,y) 称为边的剩余容量(s,v)Ef(s,v) 称作整个网络的流量。

1. 最大流

最大流:网络的最大流量。

1.1 EK 增广路算法

增广路:一条从源点到汇点的所有边的剩余容量 0 的路径。
残留网:由网络中所有结点和剩余容量大于 0 的边构成的子图,这里的边
包括有向边和其反向边。
建图时每条有向边 (x,y) 都构建一条反向边 (y,x),初始容量 c(y,x)=0
构建反向边的目的是提供一个“退流管道”,一旦前面的增广路堵死可行流可以通过“退流管道”退流,提供了“后悔机制”。

EK 算法通过 BFS 找到增广路并不断更新流量计算最大流。

算法流程:

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

时间复杂度 O(nm2)(通常适用于 103104 规模的网络)。

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 算法一次可以更新多条增广路。

du 表示节点 u 的层次,其表示 su 最少需要经过的边数。在残量网络中,满足 dv=du+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. 最小割

若一个边集 EE 被删去后,源点 s 和汇点 t 不再联通,则称该边集为网络的。记 s 所在点的集合为 St 所在点的集合为 T,则割 (S,T) 的容量 c(S,T) 表示所有从 ST 的边的容量之和。割的容量之和最小的割称为网络的最小割

最大流最小割定理:任何一个网络 G 的最大流等于最小割中边的容量之和。

证明:

假设最小割 < 最大流,那么割去这些边后,因为网络流量尚未最大化,所以仍然可以找到一条 ST 的增广路,矛盾。所以最小割 最大流。

如果我们求出了最大流 f,那么此时一定不存在 st 的增广路,即 S 的出边一定是满流,S 的入边一定是零流。那么有:f(s,t)=fout(S)fin(S)=fout(S)=c(S,T)。得证。

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

  1. 将边权改为 (m+1)ci+1,此时由于最小割边数不可能超过 m,所以此时最小割 modm 即为最小割的最小边数。
  2. 将满流的边设为 1,未满流的边设为 +,此时的最小割即为原最小割的最小边数。

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;
}

本文作者:Jasper08

本文链接:https://www.cnblogs.com/Jasper08/p/17529527.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Jasper08  阅读(11)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
🔑