网络流

网络流

基本概念

可以类比于下水管道,每个管道都有一定容积,源点是自来水厂,将水注入与源点相邻管道,经过多个中转站后到达家中,即汇点

性质

  • 容量限制:每条边的流量不可能大于该边的容量
  • 斜对称:正向边的流量 = 反向边的流量
  • 流量守恒:正向的所有流量和 = 反向的所有流量和

注:代码中每条边初始均要建出流量为 0 的反向边,叙述中为了方便均省去,要注意!


最大流

算法:\(Ek\) \(O(nm^2)\) 不优,一般网络流中虽不至于稠密图,但边数不少

\(Dinic\)

先用 bfs 只走还有流量的边把图分层,然后 dfs 找增广路,每次只找下一层的,注意每次增广成功后把边权减去流量,反边加上流量

一直进行增广直到找不到增广路为止,然后重新 bfs 分层,bfs 时源点与汇点不连通时结束

优化

  • 当前弧优化:已经增广过的就不增广了,记录一下下次从下一个没增广的开始,注意这个记录重新 bfs 时清空

  • 剪枝:从这个点出发无增广路,即流量为 0 时,把这个点在这次 bfs 中的层数清零,表示不用再找了,当流量为 0 或已流满时直接退出

复杂度:加优化后,\(O(n^2m)\)\(O(m\sqrt{\text{最大流流量}})\)

特殊图上的复杂度

单位图上:\(O(m\min\{n^{\frac 2 3},m^{\frac 1 2}\})\)

单位图:边权一样的图,边权相差小或者都是某个数的倍数的也可近似为单位图处理

code:

#include<bits/stdc++.h>
#define reg register
using namespace std;

typedef long long ll;
const ll N = 410, M = 10010, inf = INT_MAX;
ll n, m, s, t, u, v, w, head[N], nxt[M], to[M], e[M], st[N], idx = 1, ans, sum, dep[N];
inline ll read()
{
	reg char ch = getchar();	reg ll x = 0;
	while(ch < '0' || ch > '9')	ch = getchar();
	while(ch >= '0' && ch <= '9')	x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x;
}
inline void add(ll u, ll v, ll w)
{
	e[++idx] = w, nxt[idx] = head[u];
	head[u] = st[u] = idx, to[idx] = v;
}
inline ll bfs()
{
	queue<ll> q;	memset(dep, 0, sizeof(dep));
	q.push(s), dep[s] = 1;
	while(!q.empty())
	{
		ll tt = q.front();	q.pop();
		st[tt] = head[tt]; // 清零标记
		if(tt == t)	return 1; // 结束了
		for(reg ll i = head[tt]; i; i = nxt[i])
			if(e[i] > 0 && !dep[to[i]])	dep[to[i]] = dep[tt] + 1, q.push(to[i]);
	}
	return 2; // 源点汇点不连通
}
inline int dinic(int x, int flow)
{
	if(x == t || flow <= 0)	return flow; // 到了汇点或没流,返回流量
	int res = flow, k = 0;
	for(reg int i = st[x]; i; i = nxt[i])
	{
		st[x] = i; // 当前弧优化,记录标记,下次直接从这开始找
		if(e[i] > 0 && dep[to[i]] == dep[x] + 1)
		{
			k = dinic(to[i], min(e[i], res));
			if(k > 0)	e[i] -= k, e[i ^ 1] += k, res -= k; // 流过去
			else	dep[to[i]] = 0; // 剪枝优化
			if(res <= 0)	return flow; // 加上优化!不然 T飞 
		}
	}
	return flow - res;
}
int main()
{
	n = read(), m = read(), s = read(), t = read();
	for(reg ll i = 1; i <= m; ++i)
	{
		u = read(), v = read(), w = read();
		if(w)	add(u, v, w), add(v, u, 0);
	}
	while(bfs() == 1)
		while((sum = dinic(s, inf)) > 0)	ans += sum;
	printf("%lld", ans);
	return 0;
}

最小割

定理:最大流 = 最小割

跑最大流即可

最小割输出方案

从最大流构造最小割

  • 从源点出发只走不满流的边
  • 如果一条边左右两端的访问情况不同,则属于割集

应用:二分图中求最大权独立集

独立集,指选出的集合中两两间没有边

此时把二分图左部连向 \(S\),右部连向 \(T\),边权均为点权,把二分图中原来有的边连上,边权为 \(+\infty\)

最小割就是不得不舍弃的权值,用总权值-最大流即为答案


费用流

全称:最小费用最大流

此时自来水厂输水收费,每条管道每单位流量有一个费用,此时输水总量(流量)不变的情况下让总费用最小

注意:先满足最大流,再满足最小费用

算法

1.Dinic + SPFA (可能中途有负权,用 SPFA)

把上面 bfs 分层改为以每条边单位费用为边权 SPFA 跑最短路,每次只转移到最短路上下一个可能走到的点

注意 Dinic 中优化为栈优化,搜到一个点标记,回溯时取消标记,有标记的就不搜了,不能剩余流量为 0 即退出,否则回溯时标记无法清零

但是,它无法分层,仍然可能被卡

code:

inline ll spfa()
{
	memset(dis, 0x3f, sizeof(dis)), memset(book, 0, sizeof(book));
	memcpy(st, head, sizeof(st));
	queue<ll> q;	q.push(s), dis[s] = 0, book[s] = 1;
	while(!q.empty())
	{
		ll tt = q.front();	q.pop(), book[tt] = 0;
		for(reg ll i = head[tt]; i; i = nxt[i])
			if(e[i] > 0 && dis[to[i]] > dis[tt] + cost[i])	
			{
				dis[to[i]] = dis[tt] + cost[i];
				if(!book[to[i]])	q.push(to[i]), book[to[i]] = 1;
			}
	}
	if(dis[t] < 3e13)	return 1;
	return 2;
}
inline ll dinic(ll x, ll flow)
{
	if(x == t)	return flow;
	ll res = flow, flw = 0, k = 0;
	vis[x] = 1;
	for(reg ll i = st[x]; i; i = nxt[i])
	{
		if(e[i] > 0 && !vis[to[i]] && dis[to[i]] == dis[x] + cost[i])
		{
			flw = min(res, e[i]), k = dinic(to[i], flw);
			if(k)	e[i] -= k, e[i ^ 1] += k, res -= k, sum += cost[i] * k;
		}
	}
	vis[x] = 0;
	return flow - res;
}
int main()
{
	n = read(), m = read(), s = read(), t = read();
	for(reg ll i = 1; i <= m; ++i)
	{
		u = read(), v = read(), d = read(), w = read();
		add(u, v, d, w), add(v, u, 0, -w);
	}
	while(spfa() == 1)
		while((lsh = dinic(s, inf)) > 0)	ans += lsh;
	printf("%lld %lld", ans, sum);
	return 0;
}

2.EK + SPFA

此时,用 EK 这个看起来不优的算法才不被卡

EK 简介一下吧:

虽然它一次只能找一条增广路,但是它用 bfs 找,沿仍有流量的边走,找不到汇点即退出

bfs 时记录从源点流向每个点的流量(即源点到它的经过边权最小值),和它的前驱边(从哪条边流到它),最后流量就是流向汇点的

但是缺点是 bfs 时无法确定到底流哪条边,于是结束后利用前驱边手动流,把反向边和正向边处理了

费用流,就把 bfs 换成 SPFA,同理跑费用的最短路

代码:

inline int spfa() // 找最小费用到 T 路径 
{
	memset(dis, 0x3f, sizeof(dis)), book.reset();
	queue<int> q;
	q.push(S), dis[S] = 0, incf[S] = inf;
	while(!q.empty())
	{
		int t = q.front();	q.pop();
		book[t] = 0;
		for(reg int i = head[t]; i; i = nxt[i])
			if(e[i] > 0 && dis[to[i]] > dis[t] + cost[i])
			{
				dis[to[i]] = dis[t] + cost[i], pre[to[i]] = i;
				incf[to[i]] = min(incf[t], e[i]);
				if(!book[to[i]])	q.push(to[i]), book[to[i]] = 1;
			}
	}
	if(dis[T] < inf)	return 1;
	return 2;
}
int main()
{
	n = read(), m = read(), S = read(), T = read();
	for(reg int i = 1, u = 0, v = 0, d = 0, w = 0; i <= m; ++i)
	{
		u = read(), v = read(), d = read(), w = read();
		add(u, v, d, w), add(v, u, 0, -w);
	}
	while(spfa() == 1)
	{
		int nw = T, i = pre[nw];
		ans += incf[T], sum += dis[T] * incf[T];
		while(nw != S) // 手动模拟流量减少,EK 中不像 dinic 在 dfs时即减少流量 
		{
			e[i] -= incf[T], e[i ^ 1] += incf[T];
			nw = to[i ^ 1], i = pre[nw];
		}
	}
	printf("%d %d", ans, sum);
	return 0;
}

无源汇上下界可行流

对于上下界网络流的处理,通过新建附加源汇来保证流量平衡

可以想到转化:刚开始每条边的流量定为下界减去上界,跑最大流

但会出现问题:每个点初始有流量下界,先流满下界,每个点应该流入的不等于此时应该流出的,导致流量不平衡

所以怎么办?

流满下界后,计算每个点的流入与流出之差 \(d_i\),由于源汇点可不满足流量平衡,所以可以添加一些辅助边使流量平衡

  • 流入大于流出,即 \(d_i>0\),这个点理应流入也只等于流出,源点连向它补齐流量平衡造成的流入损失,流量为 \(d_i\)

  • 流入小于流出,即 \(d_i<0\),同理,它连向汇点,流量为 \(-d_i\)

从附加源跑最大流到附加汇

  • 如果流量不等于源点应该流出(或汇点应该流入)的流量,即流量还是不平衡,无可行流

  • 反之,有可行流,此时的流量就是一组解

有源汇上下界流

此时相比无源汇的情况,多了 2 个可以流量不平衡的点

那就让原图中的汇点向源点连一条流量为 \(+\infty\) 的边

由于源点多流出的流量 \(=\) 汇点多流入的流量,这样源点多流出的流量、汇点多流入的流量就可以通过此边平衡

先按无源汇点的跑一边最大流,设此时 \(T\to S\) 的边流量为 \(ans1\)

可行流

判断标准与无源汇时相同

最小流

把原图中汇点流向源点的边删掉(防死循环),再在残量网络上从原图汇点原图原点跑最大流,流量为 \(ans2\)

\(ans=ans1-ans2\)

感性理解:这是跑出可行解后还能向下浮动的最大流量

最大流

同最小流,只不过此时删边后从原图源点向原图汇点跑最大流,可浮动的流量要向上浮动,\(ans=ans1+ans2\)

code:(P5192 【模板】有源汇上下界最大流

这题建模:还比较直接,以少女和天数为点,互相据题意连边即可

#include<bits/stdc++.h>
#define reg register
#define clear(a) memset(a,0,sizeof(a))
using namespace std;

const int N = 1000500, M = 2500010, inf = 1e9;
int n, m, g, c, day, id, l, r, e[M], to[M], nxt[M], head[N], st[N], idx, dep[N], d[N], s1, t1, s, t, lsh, ans, sum;
inline void add(int u, int v, int w)
{
	e[++idx] = w, nxt[idx] = head[u];
	to[idx] = v, head[u] = idx;
}
inline int bfs()
{
	memset(dep, 0, sizeof(dep));
	queue<int> q;	q.push(s), dep[s] = 1;
	while(!q.empty())
	{
		int tt = q.front();	q.pop();
		st[tt] = head[tt];
		if(tt == t)	return 1;
		for(reg int i = head[tt]; i; i = nxt[i])
		    if(e[i] > 0 && !dep[to[i]])	q.push(to[i]), dep[to[i]] = dep[tt] + 1;
	}
	return 2;
}
inline int dinic(int x, int flow)
{
	if(x == t || flow <= 0)	return flow;
	int res = flow, k = 0;
	for(reg int i = st[x]; i; i = nxt[i])
	{
		st[x] = i;
		if(e[i] > 0 && dep[to[i]] == dep[x] + 1)
		{
			k = dinic(to[i], min(e[i], res));
			if(k > 0)	e[i] -= k, e[i ^ 1] += k, res -= k; 
			else	dep[to[i]] = 0; 
			if(res <= 0)	return flow;
		}
	}
	return flow - res;
}
int main()
{
	while(~scanf("%d%d", &n, &m))
	{
		clear(head), clear(d), ans = sum = 0;
		s1 = n + m + 1, t1 = n + m + 2, s = n + m + 3, t = n + m + 4, idx = 1;
		for(reg int i = 1; i <= m; ++i)
		{
			scanf("%d", &g), d[i + n] -= g, d[t1] += g;
			add(i + n, t1, inf - g), add(t1, i + n, 0);
		}
		for(reg int i = 1; i <= n; ++i)
		{
			scanf("%d%d", &c, &day);
			add(s1, i, day), add(i, s1, 0);
			for(reg int j = 1; j <= c; ++j)
			{
				scanf("%d%d%d", &id, &l, &r), ++id;
				add(i, id + n, r - l), add(id + n, i, 0), d[id + n] += l, d[i] -= l;
			}
		}
		for(reg int i = 1; i <= n + m + 2; ++i)
			if(d[i] > 0)	add(s, i, d[i]), add(i, s, 0), sum += d[i];
			else if(d[i] < 0)	add(i, t, -d[i]), add(t, i, 0);
		add(t1, s1, inf), add(s1, t1, 0);
		while(bfs() == 1)
			while((lsh = dinic(s, inf)) > 0)	ans += lsh; // 错的!
		if(ans != sum)
		{
			puts("-1\n");
			continue;
		}
		e[idx] = e[idx ^ 1] = 0, s = s1, t = t1;
		while(bfs() == 1)
			while((lsh = dinic(s, inf)) > 0)	ans += lsh;		
		printf("%d\n\n", ans);
	}
	return 0;
}

update on 2024.5.6:

之前的是错的!\(ans\) 一定要取 \(T\to S\) 边的流量,由于此时流量平衡这个流量就是一组可行解

而第一次 dinic 求出的和只是调整的流量!

但为什么这个错的能过 LG 模板啊……

费用流

注:这里的最小费用流指的是满足上下界要求中的流的最小费用,无需满足最大流

与普通有源汇上下界流相同,不过只用从辅助源汇点出发跑一遍最小费用最大流,答案注意加上预先流满的下界产生的费用

code:P4043 [AHOI2014/JSOI2014]支线剧情

#include<bits/stdc++.h>
#define reg register
using namespace std;

const int N = 410, M = 200010, inf = 5e8;
int n, p[N], v, w, s, t, e[M], to[M], nxt[M], head[N], cost[M], book[N], idx = 1, dep[N], st[N], d[N], ans, lsh, sum, vis[N];
inline int read()
{
	reg char ch = getchar();	reg int x = 0;
	while(ch < '0' || ch > '9')	ch = getchar();
	while(ch >= '0' && ch <= '9')	x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x;
}
inline void add(int u, int v, int w, int val)
{
	e[++idx] = w, cost[idx] = val;
	to[idx] = v, nxt[idx] = head[u], head[u] = idx;
}
inline int spfa()
{
	memset(dep, 0x3f, sizeof(dep)), memset(vis, 0, sizeof(vis));
	queue<int> q;	q.push(s), dep[s] = 1, vis[s] = 1;
	while(!q.empty())
	{
		int tt = q.front();	q.pop();
		st[tt] = head[tt], vis[tt] = 0;
		for(int i = head[tt]; i; i = nxt[i])
			if(e[i] > 0 && dep[to[i]] > dep[tt] + cost[i])	
			{
				dep[to[i]] = dep[tt] + cost[i];
				if(!vis[to[i]])	vis[to[i]] = 1, q.push(to[i]);
			}
	}
	if(dep[t] < inf)	return 1;
	return 2;
}
inline int dinic(int x, int flow)
{
	if(x == t || flow <= 0)	return flow;
	int res = flow, k = 0;
	book[x] = 1; // 注意栈优化
	for(reg int i = st[x]; i; i = nxt[i])
	{
		st[x] = i;
		if(e[i] > 0 && !book[to[i]] && dep[to[i]] == dep[x] + cost[i])
		{
			k = dinic(to[i], min(res, e[i]));
			if(k > 0)	e[i] -= k, e[i ^ 1] += k, res -= k, sum += k * cost[i];
		}
	}
	book[x] = 0;
	return flow - res;
}
int main()
{
	n = read(), s = n + 2, t = n + 3; // 辅助源汇点,真实源点:1,真实汇点:n+1
	for(reg int i = 1; i <= n; ++i)
	{
		p[i] = read();
		for(reg int j = 1; j <= p[i]; ++j)	
		{
			v = read(), w = read(), ++d[v], --d[i], sum += w;
			add(i, v, inf - 1, w), add(v, i, 0, -w);
		}
	} // 注意这里可以随时退出游戏,所有点都可以连向真实汇点
	for(reg int i = 1; i <= n; ++i)	add(i, n + 1, inf, 0), add(n + 1, i, 0, 0);
	for(reg int i = 1; i <= n; ++i)
		if(d[i] > 0)	add(s, i, d[i], 0), add(i, s, 0, 0);
		else if(d[i] < 0)	add(i, t, -d[i], 0), add(t, i, 0, 0);
	add(n + 1, 1, inf, 0), add(1, n + 1, 0, 0);
	while(spfa() == 1)
		while((lsh = dinic(s, inf)) > 0)	ans += lsh;
	printf("%d", sum);
	return 0;
}

例题与常见套路

1.P4313 文理分科

看到这种二选一的题,想到网络流

把每个人看作一个点,连 \(S\) 的边为 \(sci_i\),连 \(T\) 的边为 \(art_i\)

那相邻相同产生的贡献?新建一个点,连向 \(S/T\),边权为贡献

把这个点和对应的人和它周围的人连边权为 \(+\infty\) 的边(保证不被割掉)

这样跑最小割,发现文、理中肯定要去掉一个,如果相邻几个人割选文科的边,即选理科,那它们连接的都选文科的贡献点就必须割掉,无法产生贡献,而都选理科的点不必割掉,反之同理

如果相邻的人选理科、文科的都有,那两边的相同的贡献点都要被割掉

发现这样是符合题意的

code:

#include<bits/stdc++.h>
#define reg register
#define pb push_back
using namespace std;

const int N = 182010, M = 1220010, inf = 2e9;
int n, m, s, t, art, sci, sa, sc, num, sum, id[210][210], ans = 0, idx = 1, st[N], dep[N], lsh;
int nex[6] = {0, 0, 0, 1, -1}, ney[6] = {0, 1, -1, 0, 0}, e[M]; 
vector<int> edge[N], eid[N];
inline int read()
{
	reg char ch = getchar();	reg int x = 0;
	while(ch < '0' || ch > '9')	ch = getchar();
	while(ch >= '0' && ch <= '9')	x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x;
}
inline void add(int u, int v, int w)
{
	edge[u].pb(v), eid[u].pb(++idx), e[idx] = w;
}
inline int bfs()
{
	memset(dep, 0, sizeof(dep));
	queue<int> q;	q.push(s), dep[s] = 1;
	while(!q.empty())
	{
		int tt = q.front();	q.pop();
		st[tt] = 0;
		if(tt == t)	return 1;
		for(reg int i = 0; i < (int)edge[tt].size(); ++i)
			if(e[eid[tt][i]] > 0 && !dep[edge[tt][i]])	dep[edge[tt][i]] = dep[tt] + 1, q.push(edge[tt][i]);
	}
	return 2;
}
inline int dinic(int x, int flow)
{
	if(x == t || flow == 0)	return flow;
	int res = flow, k = 0;
	for(reg int i = st[x]; i < (int)edge[x].size(); ++i)
	{
		st[x] = i;
		if(e[eid[x][i]] > 0 && dep[edge[x][i]] == dep[x] + 1)
		{
			k = dinic(edge[x][i], min(res, e[eid[x][i]]));
			if(k > 0)	e[eid[x][i]] -= k, e[eid[x][i] ^ 1] += k, res -= k;
			else	dep[edge[x][i]] = 0;
		}
	}
	return flow - res;
}
int main()
{
	n = read(), m = read(), s = n * m + 1, t = num = n * m + 2;
	for(reg int i = 1; i <= n; ++i)
		for(reg int j = 1; j <= m; ++j)
		{
			art = read(), id[i][j] = (i - 1) * m + j, sum += art;
			add(id[i][j], t, art), add(t, id[i][j], 0);
		} 
	for(reg int i = 1; i <= n; ++i)
		for(reg int j = 1; j <= m; ++j)
		{
			sci = read(), sum += sci;
			add(id[i][j], s, 0), add(s, id[i][j], sci);
		}
	for(reg int i = 1; i <= n; ++i)
		for(reg int j = 1; j <= m; ++j)
		{
			sa = read(), sum += sa, ++num, add(num, t, sa), add(t, num, 0);
			for(reg int k = 0; k <= 4; ++k)	
			{
				int nx = i + nex[k], ny = j + ney[k];
				if(nx < 1 || ny < 1 || nx > n || ny > m)	continue;
				add(id[nx][ny], num, inf), add(num, id[nx][ny], 0);
			}
		}
	for(reg int i = 1; i <= n; ++i)
		for(reg int j = 1; j <= m; ++j)
		{
			sa = read(), sum += sa, ++num, add(num, s, 0), add(s, num, sa);
			for(reg int k = 0; k <= 4; ++k)	
			{
				int nx = i + nex[k], ny = j + ney[k];
				if(nx < 1 || ny < 1 || nx > n || ny > m)	continue;
				add(id[nx][ny], num, 0), add(num, id[nx][ny], inf);
			}
		}
	while(bfs() == 1)
		while((lsh = dinic(s, inf)) > 0)	ans += lsh;
	printf("%d", sum - ans);
	return 0;
}

2. P3227 [HNOI2013]切糕

最小割建模应该不用解释什么,\(P\times Q\) 行,每行 \(R+1\) 个点

这里 \((P,Q,R)\) 代表平面内 \((P,Q)\) 对应行的第 \(R\) 个点

红色边为关键,处理 \(D\) 的限制,\((P,Q,R)\)\((P,Q-1,R+D)\)\((P-1,Q,R+D)\) 连边,反着同理

这里一行只会被割一条边,红边不能被割

注意:如果是 \(\ge\),则有些限制越界了,代表不能选,此时如果不连限制的边会有问题

方法1:将越界的直接连向应连的那排的最后一个点,最后点连向 \(T\) 流量为 \(\infty\),无法被选,那个点也选不了

方法2:连反向边,\((u,j)\to(u,j+1)\),连 \((u,j+1)\to(u,j)\) 流量为 \(\infty\) 的边,也是为了补全限制,这样只能选一行的一个前缀与 \(S\) 相连

这个模型用来处理形如 \(x_i-x_j\le/\ge k\) 的限制

建图的 code

inline int getid(int a, int b, int h)	{return ((a - 1) * q + b - 1) * (r + 1) + h;}
s = p * q * (r + 1) + 1, t = p * q * (r + 1) + 2;
for(reg int i = 1; i <= p; ++i)
	for(reg int j = 1; j <= q; ++j)
	{ // 先建好黑色边
		add(s, getid(i, j, 1), inf), add(getid(i, j, 1), s, 0);
		add(getid(i, j, r + 1), t, inf), add(t, getid(i, j, r + 1), 0);
		for(reg int k = 1; k <= r; ++k)	
			add(getid(i, j, k), getid(i, j, k + 1), v[i][j][k]), add(getid(i, j, k + 1), getid(i, j, k), 0);
		for(reg int k = 1; k <= r - d; ++k)
		{ // 建红色边
			if(i - 1)	
			{
				add(getid(i - 1, j, k + d), getid(i, j, k), inf), add(getid(i, j, k), getid(i - 1, j, k + d), 0);
				add(getid(i, j, k + d), getid(i - 1, j, k), inf), add(getid(i - 1, j, k), getid(i, j, k + d), 0);
			}
			if(j - 1)	
			{
				add(getid(i, j - 1, k + d), getid(i, j, k), inf), add(getid(i, j, k), getid(i, j - 1, k + d), 0);
				add(getid(i, j, k + d), getid(i, j - 1, k), inf), add(getid(i, j - 1, k), getid(i, j, k + d), 0);
			}
		}
	}
posted @ 2023-01-12 22:33  KellyWLJ  阅读(7)  评论(0编辑  收藏  举报  来源