[算法] A LITTLE 网络流

简介

所谓网络流,就是给了一张图,有源点和汇点,让你求从源点放水,到汇点的水最多能有多少;

这实际上是一个最大流的问题;

最大流

我们把这张图的每个边看作一条水管,每个水管都有一个容量,那么对于一条从源点到汇点的路径,其最大通过量是这些水管中容量最小的那一个的容量;

有个定理,叫最大流最小割定理

对于这个问题,我们有如下的处理方法:

EK 算法

定义一条增广路为从源点(设为s)到汇点(设为t)的一条路径,满足其所有边的剩余容量非负;

对于此算法,我们每次都找一条增广路,然后更新每条边的权值直到剩余图中(也即残量网络)没有增广路;

因为我们要重新找增广路,所以还要建反向边,便于回去重新找;

可以看出,它的复杂度瓶颈在边数

那么最坏情况是每次只能确定一条边,时间复杂度 $ O(nm^2) $,不太适用于稠密图,但实际使用其实达不到此上界;

因为下一个算法使用较普遍,所以这里就不放代码;

Dinic 算法

考虑对于一个残量网络,EK算法会重新遍历整个残量网络,然后只找出一条增广路,考虑将复杂度瓶颈由边转移到点;

于是有了Dinic算法;

首先对图进行分层(就是进行一边BFS,然后将遍历顺序(即深度)相同的点纳入同一层);

然后每次处理一层的所有点的增广路,直到残量网络不能分层(即s不能到达t);

我们发现,它和EK的本质区别在于它是以点为单位(每次分层是 $ \Theta(n) $ 的)找增广路,而前者是以边为单位;

时间复杂度: $ O(n^2m) $;

当然,它还有两个优化:当前弧优化和剪枝;

前者是在当前层中只去找能扩展的边,后者是去掉增广完毕的点;

点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
int n, m, s, t;
struct sss{
	int t, ne;
	long long w;
}e[200005];
int h[200005], cnt;
void add(int u, int v, long long ww) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	h[u] = cnt;
	e[cnt].w = ww;
}
int now[200005];
long long ans;
int dis[205];
bool bfs() { //点分层;
	queue<int> q;
	for (int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
	q.push(s);
	now[s] = h[s];
	dis[s] = 0;
	while(!q.empty()) {
		int tt = q.front();
		q.pop();
		for (int i = h[tt]; i; i = e[i].ne) {
			int u = e[i].t;
			if (e[i].w > 0 && dis[u] == 0x3f3f3f3f) {
				dis[u] = dis[tt] + 1;
				now[u] = h[u];
				q.push(u);
				// if (u == t) return true;
			}
		}
	}
	if (dis[t] != 0x3f3f3f3f) return true;
	else return false;
}
long long dfs(int x, long long sum) {
	if (x == t) return sum;
	long long k = 0; //当前最小剩余流量;
	long long res = 0; //流过点x的总流量;
	for (int i = now[x]; i; i = e[i].ne) {
		int u = e[i].t; 
		now[x] = i; //当前弧优化;
		if (e[i].w > 0 && (dis[u] == dis[x] + 1)) {
			k = dfs(u, min(sum, e[i].w));
			if (k == 0) dis[u] = 0x3f3f3f3f; //剪枝;
			e[i].w -= k;
			e[i ^ 1].w += k;
			res += k;
			sum -= k; //sum是经过该点的剩余流量;
		}
	}
	return res;
}
void Dinic() {
	while(bfs()) {
		ans += dfs(s, 0x3f3f3f3f3f3f3f3f);
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m >> s >> t;
	int u, v;
	long long w;
	cnt = 1;
	for (int i = 1; i <= m; i++) {
		cin >> u >> v >> w;
		add(u, v, w);
		add(v, u, 0);
	}
	Dinic();
	cout << ans;
	return 0;
}

费用流

这里主要研究最小费用最大流

给每条路赋一个单位费用 $ c $ 表示每流过 $ a $ 的流时,需要花费 $ a \times c $ 的代价;

求在满足最大流的前提下,我们的最小费用是多少;

我们想到Dinic的分层操作,其实我们可以把其替换成一个最短路算法(如SPFA),算单位费用的最短路,每次找增广路时判断一下这条路是否在增广路中即可;

注意这时因为没有层数的限制,所以一个点可能被遍历多次以致死循环,用一个vis数组标记一下即可;

时间复杂度:设最大流为 $ F $,SPFA理论上界为 $ \Theta(nm) $,则时间复杂度为: $ O(nmF) $(每次只减 $ 1 $),但SPFA一般不会达到上界,且 $ F $ 一般也不会达到,所以实际要快很多;

其实网络流这里只有两种复杂度: $ \Theta (能过) $ 和 $ \Theta (不能过) $;

例题:Luogu P3381 【模板】最小费用最大流

点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
int n, m, s, t;
struct sss{
	int t, ne;
	long long w, cos;
}e[200005];
int h[200005], cnt;
void add(int u, int v, long long ww, long long co) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	h[u] = cnt;
	e[cnt].w = ww;
	e[cnt].cos = co;
}
int now[200005];
long long ans, an;
int dis[5005];
bool vis[5005];
bool SPFA() {
	memset(dis, 0x3f, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	now[s] = h[s];
	queue<int> q;
	q.push(s);
	dis[s] = 0;
	vis[s] = true;
	while(!q.empty()) {
		int tt = q.front();
		q.pop();
		vis[tt] = false;
		for (int i = h[tt]; i; i = e[i].ne) {
			int u = e[i].t;
			if (e[i].w > 0 && dis[u] > dis[tt] + e[i].cos) {
				dis[u] = dis[tt] + e[i].cos;
				now[u] = h[u];
				if (!vis[u]) {
					vis[u] = true;
					q.push(u);
				}
			}
		}
	}
	if (dis[t] != 0x3f3f3f3f) return true;
	else return false;
}
long long dfs(int x, long long sum) {
	if (x == t) return sum;
	long long k = 0;
	long long res = 0;
	vis[x] = true;
	for (int i = now[x]; i; i = e[i].ne) {
		int u = e[i].t;
		now[x] = i;
		if (!vis[u] && e[i].w > 0 && (dis[u] == dis[x] + e[i].cos)) {
			k = dfs(u, min(sum, e[i].w));
			if (k == 0) dis[u] = 0x3f3f3f3f;
			e[i].w -= k;
			e[i ^ 1].w += k;
			res += k;
			sum -= k;
			an += e[i].cos * k;
		}
	}
	return res;
}
void Dinic() {
	while(SPFA()) {
		ans += dfs(s, 0x3f3f3f3f3f3f3f3f);
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m >> s >> t;
	int u, v;
	long long w, co;
	cnt = 1;
	for (int i = 1; i <= m; i++) {
		cin >> u >> v >> w >> co;
		add(u, v, w, co);
		add(v, u, 0, -co);
	}
	Dinic();
	cout << ans << ' ' << an;
	return 0;
}

网络流24题

主要是建模;

LOJ P6000 搭配飞行员

对于这道题,我们虚拟一个原点 $ s $,一个汇点 $ t $,然后将 $ s $ 与每个第一飞行员连一条边权为 $ 1 $ 的边,将每个第二飞行员与 $ t $ 连一条边权为 $ 1 $ 的边,将每个第一飞行员与其配对的第二飞行员连一条边权为 $ INF $ 或 $ 1 $ 的边,跑一个最大流即可;

正确性:因为每个可能的流最大流量为 $ 1 $,所以求出的最大流即为答案;

当然也可以用二分图最大匹配做;

网络流
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
int n, m;
struct sss{
	int t, ne, w;
}e[5005];
int h[5005], cnt;
void add(int u, int v, int ww) {
	e[++cnt].t = v;
	e[cnt].w = ww;
	e[cnt].ne = h[u];
	h[u] = cnt;
}
int ans;
int dis[3005], now[3005];
bool bfs() {
	queue<int> q;
	q.push(1);
	for (int i = 1; i <= n + 2; i++) dis[i] = 0x3f3f3f3f;
	dis[1] = 0;
	now[1] = h[1];
	while(!q.empty()) {
		int t = q.front();
		q.pop();
		for (int i = h[t]; i; i = e[i].ne) {
			int u = e[i].t;
			if (e[i].w > 0 && dis[u] == 0x3f3f3f3f) {
				q.push(u);
				dis[u] = dis[t] + 1;
				now[u] = h[u];
				if (u == n + 2) return true;
			}
		}
	}
	return false;
}
int dfs(int x, int sum) {
	if (x == n + 2) return sum;
	int k = 0;
	int res = 0;
	for (int i = now[x]; i; i = e[i].ne) {
		int u = e[i].t;
		now[x] = i;
		if (e[i].w > 0 && dis[u] == dis[x] + 1) {
			k = dfs(u, min(sum, e[i].w));
			res += k;
			sum -= k;
			e[i].w -= k;
			e[i ^ 1].w += k;
		}
	}
	return res;
}
void Dinic() {
	while(bfs()) {
		ans += dfs(1, 0x3f3f3f3f);
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	int a, b;
	cnt = 1;
	while(cin >> a >> b) {
		add(a + 1, b + 1, 1);
		add(b + 1, a + 1, 0);
	}
	for (int i = 1; i <= m; i++) {
		add(1, i + 1, 1);
		add(i + 1, 1, 0);
	}
	for (int i = m + 1; i <= n; i++) {
		add(i + 1, n + 2, 1);
		add(n + 2, i + 1, 0);
	}
	Dinic();
	cout << ans;
	return 0;
}
二分图最大匹配
#include <iostream>
#include <cstring>
using namespace std;
int n, m;
int g[1005][1005];
bool vis[1005];
int mat[1005];
bool hgy(int x) {
	for (int i = 0; i <= n; i++) {
		if (g[x][i] && !vis[i]) {
			vis[i] = true;
			if (!mat[i] || hgy(mat[i])) {
				mat[i] = x;
				return true;
			}
		}
	}
	return false;
}
int main() {
	cin >> n >> m;
	int a, b;
	while(cin >> a >> b) {
		g[a][b] = 1;
		g[b][a] = 1;
	}
	int ans = 0;
	for (int i = 1; i <= m; i++) {
		memset(vis, 0, sizeof(vis));
		if (hgy(i)) {
			ans++;
		}
	}
	cout << ans;
	return 0;
}

LOJ P6001 太空飞行计划

这个题用到了最大权闭合子图转最小割的建模方法;

考虑将所有实验向其所需要的仪器连有向边,实验的点权为得到的价钱,仪器的点权为花费的价钱(负数),那么我们要找到一个点权和最大的子图 $ G(V, E) $,满足其 $ E $ 中没有指向外部的边(及 $ E $ 的两个端点都在 $ V $ 中),且 $ V $ 中没有 $ E $ 的两个端点所不含有的点,这就是最大权闭合子图,也就是我们要求的;

考虑如何转化;

我们将原点 $ s $ 与所有实验连一条以其价值为边权的有向边,将每个实验与其依赖的仪器连一条边权为 $ INF $ 的有向边,将所有仪器与汇点 $ t $ 连一条边权为其花费的绝对值的有向边,则所有实验的价值和 $ - $ 其最小割即为所求;

考虑正确性,这样减去相当于将两个集合的意义全部取反,那么答案为选的实验 $ - $ 依赖的仪器,且被减数最小而且满足要求(因为求得是最小割),所以答案正确(建议自己想想);

考虑输出路径,只要 $ Dinic $ 最后一次分层时分到了即可输出(建议自己画一画);

输入挺难受,可以看看代码;

点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
int m, n;
int s, t;
struct sss{
	int t, ne, w;
}e[5005];
int h[5005], cnt;
void add(int u, int v, int ww) {
	e[++cnt].t = v;
	e[cnt].w = ww;
	e[cnt].ne = h[u];
	h[u] = cnt;
}
char c;
int ans, sum;
int dis[5005], now[5005];
bool bfs() {
	for (int i = s; i <= t; i++) dis[i] = 0x3f3f3f3f;
	dis[s] = 0;
	now[s] = h[s];
	queue<int> q;
	q.push(s);
	while(!q.empty()) {
		int tt = q.front();
		q.pop();
		for (int i = h[tt]; i; i = e[i].ne) {
			int u = e[i].t;
			if (e[i].w > 0 && dis[u] == 0x3f3f3f3f) {
				dis[u] = dis[tt] + 1;
				now[u] = h[u];
				q.push(u);
			}
		}
	}
	if (dis[t] == 0x3f3f3f3f) return false;
	else return true;
}
int dfs(int x, int sum) {
	if (x == t) return sum;
	int k = 0;
	int res = 0;
	for (int i = now[x]; i; i = e[i].ne) {
		int u = e[i].t;
		now[x] = i;
		if (e[i].w > 0 && dis[u] == dis[x] + 1) {
			k = dfs(u, min(e[i].w, sum));
			res += k;
			sum -= k;
			e[i].w -= k;
			e[i ^ 1].w += k;
		}
	}
	return res;
}
void Dinic() {
	while(bfs()) {
		ans += dfs(s, 0x3f3f3f3f);
	}
}
int main() {
	scanf("%d %d", &m, &n);
	s = 1;
	t = n + m + 2;
	int x, a;
	cnt = 1;
	for (int i = 1; i <= m; i++) {
		scanf("%d", &x);
		sum += x;
		add(s, i + 1, x);
		add(i + 1, s, 0);
		while(1) {
			do {
				c = getchar();
			} while(c == ' ');
			ungetc(c, stdin); //回退操作,将 c 退回到标准输入流;
			if (c == '\n' || c == '\r') break;
			scanf("%d", &a);
			add(i + 1, m + 1 + a, 0x3f3f3f3f);
			add(m + 1 + a, i + 1, 0);
		}
	}
	for (int i = 1; i <= n; i++) {
		scanf("%d", &x);
		add(i + m + 1, t, x);
		add(t, i + m + 1, 0);
	}
	Dinic();
	for (int i = 2; i <= m + 1; i++) {
		if (dis[i] != 0x3f3f3f3f) printf("%d ", i - 1);
	}
	printf("\n");
	for (int i = m + 2; i <= n + m + 1; i++) {
		if (dis[i] != 0x3f3f3f3f) printf("%d ", i - m - 1);
	}
	printf("\n");
	printf("%d", sum - ans);
	return 0;
}
posted @ 2024-09-26 10:54  Peppa_Even_Pig  阅读(32)  评论(1编辑  收藏  举报