@总结 - 8@ 上下界网络流等一类网络流问题


@0 - 参考资料@

menci 的博客
liu_runda 的博客

@1 - 问题引入@

我们知道,通常情况下,一个合法的流应该具有如下几个性质:
(1)(除源点汇点以外)流量守恒:\(\sum f(i, u) = \sum f(u, j)\)
(2)斜对称性:\(f(u, v) = -f(v, u)\)
(3)容量限制:\(f(u, v) \le c(u, v)\)

但在某些问题中,我们还要求边的流量有下界,即 \(l(u, v) \le f(u, v) \le c(u, v)\)

我们需要将这种流量带有上下界的网络流进行模型的转化,使得可以使用通常的网络流算法解决。

@2 - 上下界可行流@

什么是可行流?简单说即不包含源汇的,满足流量守恒、斜对称性、流量上下界的流。
如果不包含下界,零流就是一个可行流。

根据定义我们有 \(f(u, v) = l(u, v) + f'(u, v)\),其中 \(0 \le f'(u, v) \le c(u, v) - l(u, v)\),可以发现 \(f'(u, v)\) 是一个没有下界的流。
我们将原来的边 \((u, v)\) 的上界改成 \(c(u, v) - l(u, v)\),再进行下一步的处理。

我们考虑用一个等价的东西代替 \(l(u, v)\) 这部分流的。
考虑 \(l(u, v)\) 这部分流,它只会对 “流量守恒” 这一个条件造成影响:它贡献了点 \(v\) 流量为 \(l(u, v)\) 的入流,贡献了点 \(u\) 流量为 \(l(u, v)\) 的出流。

我们新建源点 \(ss\),汇点 \(tt\)。对于边 \((u, v)\),我们新建边 \((ss, v), (u, tt)\),使得它们的容量都为 \(l(u, v)\)
这样,假如所有的 \((ss, v)\), \((u, tt)\) 都满流,它们就可以起到和 \(l(u, v)\) 一样的作用。

找出 \(ss\)\(tt\) 的最大流即可,如果满流说明有一个可行解。此时边 \((u, v)\) 的流量就是 \(f'(u, v)\)(不是 \(f(u, v)\) 哦,\(f(u, v) = l(u, v) + f'(u, v)\))。

这样的确就是一个完整的算法了。但是我们还可以进行建图上的优化。
如果同时有边 \((ss, u), (u, tt)\),可以合并成一条边。如果有多条 \((ss, u)/(u, tt)\),也可以合并成一条边。
具体的话,可以统计 \(ss\)\(u\) 的容量和 \(-\) \(u\)\(tt\) 的容量和,根据其正负以及大小来建边。

@3 - 上下界最大流/最小流@

考虑求解带源汇的可行流(因为最大流/最小流必须要在有源汇的情况下才能被定义):

我们汇点 \(t\) 向源点 \(s\) 连一条容量为 \(inf\) 的边即可。
此时因为流量守恒, \(f'(t, s)\) 就等于当前可行流中 \(s\)\(t\) 的流量。

考虑求解最大流:
直接源点 \(s\) 向汇点 \(t\) 增广即可。

首先,\(ss\) 没有入边,\(tt\) 没有出边,增广路不会经过它们俩。
然后,因为斜对称性, \((t, s)\) 有一个流量为 \(-f'(t, s)\) 的反向边。增广的时候必然会经过这个反向边,就把初始可行流中 \(s\)\(t\) 的流量统计进去了。
而且因此,也不需要特意去加上某些边的流量下界(因为这个是包含下界的可行流哦)。
最后,因为我们的下界是转换成等价形式了。因此如果不修改 \(ss, tt\) 的流量,就不会出现不满足下界的情况。

考虑求解最小流(没有下界的话,最小流就是零流):
先去掉 \((t, s)\),汇点 \(t\) 向源点 \(s\) 增广。最后用 \(f'(t, s)\) \(-\) 增广得到的最大流就是答案。

t 向 s 增广即退流操作,退的越多自然流量就越小。其他的和上面差不多。

@4 - 上下界费用流@

(先不考虑负环的问题)

考虑求解无源汇的可行流。
建图部分和最大流差不多,在最后 \(ss\)\(tt\) 跑最小费用最大流即可。

考虑求解有源汇的最小费用最大流。
\(t\)\(s\) 连容量 \(inf\),费用 \(0\) 的边,跑最小费用最大流,再 \(s\)\(t\) 求最小费用最大流。

考虑求解有源汇的最小费用流。
\(t\)\(s\) 连容量 \(inf\),费用 \(0\) 的边,跑最小费用最大流,再 \(s\)\(t\) 求最小费用流。

其实和上面的最大流问题模型差不多。

@5 - 带负环的最小费用流@

这个……尽管和上下界网络流已经没关系了,但是鉴于它们的思路有一定的相似性,我还是在这里提一下。

考虑求解最小费用循环流。
什么是最小费用循环流?实际上就是合法的,费用最小的无源汇的流。

一个算法是:找到一个负权环,将它加入答案。可以发现这个算法效率不高。

一个想法是:我们可以先贪心地选择所有的负权边,再花费最小代价将它调整为合法的流(不会证明,直观上是对的)。

调整这一步,是不是比较类似于上下界网络流呢?
但是,和上下界网络流不同的是,这里是预先流满(可以取消选择),而上下界网络流是强制流满(不能取消选择)。

怎么实现撤回呢?根据斜对称性,一条边的流的减少等价于它反向边的流的增加。

所以我们这样来建图:
对于负权边 \((u, v)\),连 \((ss, v), (u, tt)\),费用为 0;连 \((v, u)\),费用为 \(-w(u, v)\)。三条边的容量都为 \(c(u, v)\)
对于其他边,保持不变。
最后 \(ss\)\(tt\) 跑最小费用最大流,答案为 最大流的费用 + 所有的负权边的权值和。

建图方面,是不是和上下界网络流也有一定的共同点?
这是一个通用套路。

带源汇的话,一样是 \(t\)\(s\) 连容量 \(inf\),费用 \(0\) 的边。
这也是一个通用套路。

@6 - 例题与参考代码实现@

上下界可行流模板题 sgu 194:Reactor Cooling
【sgu 都搬去和 cf 一个网站了……然而 vjudge 还是没有更新】
【51nod 换网址了……然而 vjudge 还是没有更新】
update in 2020/06/01:然而它更新了,而且加入了loj。

参考代码:

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 200 + 5;
const int MAXE = 2*200*200 + 5;
const int INF = 1<<30;
struct flow_graph{
	struct edge{
		int to, flow, cap;
		edge *nxt, *rev;
	}e[MAXE], *adj[MAXV], *ecnt;
	int d[MAXV], vd[MAXV], s, t;
	void init() {
		ecnt = &e[0];
		for(int i=0;i<MAXV;i++)
			adj[i] = NULL, d[i] = vd[i] = 0;
	}
	void addedge(int u, int v, int c) {
		edge *p = (++ecnt), *q = (++ecnt);
		p->to = v, p->cap = c, p->flow = 0;
		p->nxt = adj[u], adj[u] = p;
		q->to = u, q->cap = 0, q->flow = 0;
		q->nxt = adj[v], adj[v] = q;
		p->rev = q, q->rev = p;
	}
	int aug(int x, int tot) {
		if( x == t ) return tot;
		int mind = t + 1, sum = 0;
		for(edge *p=adj[x];p;p=p->nxt) {
			if( p->cap > p->flow ) {
				if( d[x] == d[p->to] + 1 ) {
					int del = aug(p->to, min(tot-sum, p->cap-p->flow));
					p->flow += del, p->rev->flow -= del, sum += del;
					if( sum == tot || d[s] > t + 1 ) return sum;
				}
				mind = min(mind, d[p->to]);
			}
		}
		if( !sum ) {
			vd[d[x]]--;
			if( !vd[d[x]] ) {
				d[s] = t + 2;
				return sum;
			}
			d[x] = mind + 1;
			vd[d[x]]++;
		}
		return sum;
	}
	int max_flow(int _s, int _t) {
		int flow = 0; s = _s, t = _t;
		while( d[s] <= t + 1 )
			flow += aug(s, INF);
		return flow;
	}
}G;
struct edge{
	int u, v, l, c;
}e[MAXE];
int deg[MAXV];
int main() {
	int N, M; scanf("%d%d", &N, &M); G.init();
	for(int i=1;i<=M;i++) {
		scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].l, &e[i].c);
		deg[e[i].u] -= e[i].l, deg[e[i].v] += e[i].l; G.addedge(e[i].u, e[i].v, e[i].c - e[i].l);
	}
	int tot = 0;
	for(int i=1;i<=N;i++)
		if( deg[i] < 0 ) G.addedge(i, N + 1, -deg[i]);
		else if( deg[i] > 0 ) G.addedge(0, i, deg[i]), tot += deg[i];
	if( G.max_flow(0, N + 1) == tot ) {
		puts("YES");
		for(int i=1;i<=M;i++)
			printf("%d\n", e[i].l + G.e[2*i-1].flow);
	}
	else puts("NO");
}

上下界最大流问题 zoj 3229:Shoot the Bullet(东方文花帖)
建模比较简单,为了不剧透,附在代码最末。
参考代码:

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 1500 + 5;
const int MAXE = MAXV*100 + 5;
const int INF = int(1E9);
struct FlowGraph{
	struct edge{
		int to, flow, cap;
		edge *nxt, *rev;
	}edges[MAXE], *adj[MAXV], *ecnt;
	int s, t, n, d[MAXV], vd[MAXV];
	void init() {
		ecnt = &edges[0];
		for(int i=0;i<MAXV;i++) adj[i] = NULL;
	}
	void addedge(int u, int v, int c) {
		edge *p = (++ecnt), *q = (++ecnt);
		p->to = v, p->flow = 0, p->cap = c;
		p->nxt = adj[u], adj[u] = p;
		q->to = u, q->flow = 0, q->cap = 0;
		q->nxt = adj[v], adj[v] = q;
		p->rev = q, q->rev = p;
	}
	int aug(int x, int tot) {
		if( x == t ) return tot;
		int sum = 0, mind = n + 1;
		for(edge *p=adj[x];p;p=p->nxt) {
			if( p->cap > p->flow ) {
				if( d[p->to] + 1 == d[x] ) {
					int del = aug(p->to, min(tot-sum, p->cap-p->flow));
					p->flow += del, p->rev->flow -= del, sum += del;
					if( sum == tot || d[s] > n ) return sum;
				}
				mind = min(mind, d[p->to]);
			}
		}
		if( sum == 0 ) {
			vd[d[x]]--;
			if( vd[d[x]] == 0 )
				d[s] = n + 1;
			d[x] = mind + 1;
			vd[d[x]]++;
		}
		return sum;
	}
	int max_flow(int _s, int _t, int _n) {
		s = _s, t = _t, n = _n;
		for(int i=0;i<MAXV;i++) d[i] = vd[i] = 0;
		int flow = 0;
		while( d[s] <= n )
			flow += aug(s, INF);
		return flow;
	}
}G;
int deg[MAXV], C[MAXV], L[500 + 5][1000 + 5];
int main() {
	int n, m;
	while( scanf("%d%d", &n, &m) == 2 ) {
		G.init(); int s = n + m + 1, t = n + m + 2;
		for(int i=1;i<=m;i++) {
			int x; scanf("%d", &x);
			G.addedge(i, t, INF);
			deg[t] += x, deg[i] -= x;
		}
		for(int i=1;i<=n;i++) {
			int D; scanf("%d%d", &C[i], &D);
			G.addedge(s, m + i, D);
			for(int j=1;j<=C[i];j++) {
				int T, R; scanf("%d%d%d", &T, &L[i][j], &R), T++;
				G.addedge(m + i, T, R - L[i][j]);
				deg[T] += L[i][j], deg[m + i] -= L[i][j];
			}
		}
		int tot = 0;
		for(int i=1;i<=n+m+2;i++) {
			if( deg[i] > 0 ) G.addedge(0, i, deg[i]), tot += deg[i];
			if( deg[i] < 0 ) G.addedge(i, n + m + 3, -deg[i]);
			deg[i] = 0;
		}
		G.addedge(t, s, INF);
		if( G.max_flow(0, n + m + 3, n + m + 3) == tot ) {
			printf("%d\n", G.max_flow(s, t, n + m + 3));
			for(int i=1;i<=n;i++) {
				int j = C[i];
				for(FlowGraph::edge *p=G.adj[m + i];p;p=p->nxt)
					if( p->to <= m && p->to >= 1 ) L[i][j] += p->flow, j--;
			}
			for(int i=1;i<=n;i++)
				for(int j=1;j<=C[i];j++)
					printf("%d\n", L[i][j]);
		}
		else printf("-1\n");
		puts("");
	}
}//MLE 报 Segmentation Fault, RE 也报 Segmentation Fault……
/*
建成二分图。
左边一排 n 个点表示 n 天,由源点连过来,容量为 D。
右边一排 m 个点表示 m 个女孩,连向汇点,下界为 G。
第 i 天向 Ci 个女孩连流量在 [Lij, Rij] 的边。 
*/

上下界最小流问题 bzoj 2502:清理雪道
一样的,建模附在最后面。
参考代码:

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 100 + 5;
const int MAXE = MAXV*MAXV + 5;
const int INF = int(1E9);
struct FlowGraph{
	struct edge{
		int to, flow, cap;
		edge *nxt, *rev;
	}edges[MAXE], *adj[MAXV], *ecnt;
	int s, t, n, d[MAXV], vd[MAXV];
	void init() {
		ecnt = &edges[0];
		for(int i=0;i<MAXV;i++) adj[i] = NULL;
	}
	void addedge(int u, int v, int c) {
		edge *p = (++ecnt), *q = (++ecnt);
		p->to = v, p->flow = 0, p->cap = c;
		p->nxt = adj[u], adj[u] = p;
		q->to = u, q->flow = 0, q->cap = 0;
		q->nxt = adj[v], adj[v] = q;
		p->rev = q, q->rev = p;
	}
	int aug(int x, int tot) {
		if( x == t ) return tot;
		int sum = 0, mind = n + 1;
		for(edge *p=adj[x];p;p=p->nxt) {
			if( p->cap > p->flow ) {
				if( d[p->to] + 1 == d[x] ) {
					int del = aug(p->to, min(tot-sum, p->cap-p->flow));
					p->flow += del, p->rev->flow -= del, sum += del;
					if( sum == tot || d[s] > n ) return sum;
				}
				mind = min(mind, d[p->to]);
			}
		}
		if( sum == 0 ) {
			vd[d[x]]--;
			if( vd[d[x]] == 0 )
				d[s] = n + 1;
			d[x] = mind + 1;
			vd[d[x]]++;
		}
		return sum;
	}
	int max_flow(int _s, int _t, int _n) {
		s = _s, t = _t, n = _n;
		for(int i=0;i<MAXV;i++) d[i] = vd[i] = 0;
		int flow = 0;
		while( d[s] <= n )
			flow += aug(s, INF);
		return flow;
	}
}G;
int deg[MAXV];
int main() {
	G.init(); int N; scanf("%d", &N);
	int s = N + 1, t = N + 2, ss = 0, tt = N + 3;
	for(int i=1;i<=N;i++) {
		int K; scanf("%d", &K);
		deg[i] += K;
		for(int j=1;j<=K;j++) {
			int B; scanf("%d", &B);
			G.addedge(i, B, INF); deg[B]--;
		}
		G.addedge(s, i, INF);
		G.addedge(i, t, INF);
	}
	for(int i=1;i<=N;i++) {
		if( deg[i] < 0 ) G.addedge(ss, i, -deg[i]);
		if( deg[i] > 0 ) G.addedge(i, tt, deg[i]);
	}
	G.addedge(t, s, INF);
	G.max_flow(ss, tt, tt);
	int ans = G.adj[t]->flow;
	G.adj[s] = G.adj[s]->nxt, G.adj[t] = G.adj[t]->nxt;
	printf("%d\n", ans - G.max_flow(t, s, tt));
}
/*
每个点都可以作为起点/终点:源点连每个点/每个点连汇点。
必须经过即流量下界为 1。

这或许启发我们 DAG 中的最小路径覆盖/有向图中的最小环覆盖 也可以转换为上下界网络流的模型。
*/

上下界最小费用流问题 bzoj 3876: [Ahoi2014&Jsoi2014]支线剧情
建模附在代码最后。
参考代码:

#include<queue>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 400 + 5;
const int MAXE = 20000 + 5;
const int INF = int(1E9);
struct FlowGraph{
	struct edge{
		int to, flow, cap, dis;
		edge *nxt, *rev;
	}edges[MAXE], *adj[MAXV], *cur[MAXV], *ecnt;
	int s, t, cost, dist[MAXV];
	void init() {
		ecnt = &edges[0];
		for(int i=0;i<MAXV;i++) adj[i] = NULL;
	}
	void addedge(int u, int v, int c, int w) {
		edge *p = (++ecnt), *q = (++ecnt);
		p->to = v, p->flow = 0, p->cap = c, p->dis = w;
		p->nxt = adj[u], adj[u] = p;
		q->to = u, q->flow = 0, q->cap = 0, q->dis = -w;
		q->nxt = adj[v], adj[v] = q;
		p->rev = q, q->rev = p;
	}
	bool inque[MAXV];
	bool relabel() {
		queue<int>que;
		for(int i=0;i<MAXV;i++) dist[i] = INF, cur[i] = adj[i];
		que.push(s); dist[s] = 0, inque[s] = true;
		while( !que.empty() ) {
			int f = que.front(); que.pop(); inque[f] = false;
			for(edge *p=adj[f];p;p=p->nxt) {
				if( p->cap > p->flow && dist[f] + p->dis < dist[p->to] ) {
					dist[p->to] = dist[f] + p->dis;
					if( !inque[p->to] ) {
						que.push(p->to);
						inque[p->to] = true;
					}
				}
			}
		}
		return !(dist[t] == INF);
	}
	bool vis[MAXV];
	int aug(int x, int tot) {
		if( x == t ) {
			cost += tot*dist[x];
			return tot;
		}
		int sum = 0; vis[x] = true;
		for(edge *&p=cur[x];p;p=p->nxt) {
			if( p->cap > p->flow && !vis[p->to] && dist[p->to] == dist[x] + p->dis ) {
				int del = aug(p->to, min(tot-sum, p->cap-p->flow));
				p->flow += del, p->rev->flow -= del, sum += del;
				if( sum == tot ) break;
			}
		}
		vis[x] = false;
		return sum;
	}
	int min_cost_max_flow(int _s, int _t) {
		s = _s, t = _t; int flow = 0; cost = 0;
		while( relabel() )
			flow += aug(s, INF);
		return flow;
	}
}G;
int deg[MAXV];
int main() {
	G.init(); int N; scanf("%d", &N);
	int s = N + 1, t = N + 2, ss = 0, tt = N + 3, ans = 0;
	for(int i=1;i<=N;i++) {
		int K; scanf("%d", &K);
		deg[i] += K;
		for(int j=1;j<=K;j++) {
			int B, T; scanf("%d%d", &B, &T);
			G.addedge(i, B, INF, T); deg[B]--;
			ans += T;
		}
		G.addedge(i, t, INF, 0);
	}
	G.addedge(s, 1, INF, 0);
	for(int i=1;i<=N;i++) {
		if( deg[i] < 0 ) G.addedge(ss, i, -deg[i], 0);
		if( deg[i] > 0 ) G.addedge(i, tt, deg[i], 0);
	}
	G.addedge(t, s, INF, 0);
	G.min_cost_max_flow(ss, tt);
	printf("%d\n", ans + G.cost);
}
/*
和上一题比较类似,只是给定了边的费用,并且规定起点必须在 1。
注意因为原图(包含源汇 s, t 的那个图)是一个 DAG,所以从 ss 出发的流必然会经过 s 才会回到 tt。
即我们不需要最后再从 s 开始增广一遍。
*/

@7 - 一些类似的杂题@

poj 1637:Sightseeing tour(混合图欧拉回路问题)
这是一个比较经典的问题。思路是:尝试给无向边定向,使得每个点的入度等于其出度。
入度等于出度?是不是很像网络流中的流量守恒。

我们先随机给无向边定向,再进行调整(把某些无向边反向)。
考虑反向一条无向边会发生什么:
相当于这条边的流量-1,它反向边的流量+1。
相当于这条边的流量-2。

因为是欧拉回路,每个点的度数必须要为偶数。我们不妨给所有边的流量 / 2。
这样,调整一条边变成了给这条边的流量-1。
这就十分网络流了。直接按照上下界网络流的思路来使得它流量守恒。

一份参考代码。

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 200 + 5;
const int MAXE = 5000 + 5;
const int INF = 1<<30;
struct FlowGraph{
	struct edge{
		int to, flow, cap;
		edge *nxt, *rev;
	}edges[MAXE], *adj[MAXV], *ecnt;
	int s, t, d[MAXV], vd[MAXV];
	void init() {
		ecnt = &edges[0];
		for(int i=0;i<MAXV;i++)
			adj[i] = NULL, d[i] = vd[i] = 0;
	}
	void addedge(int u, int v, int c) {
		edge *p = (++ecnt), *q = (++ecnt);
		p->to = v, p->cap = c, p->flow = 0;
		p->nxt = adj[u], adj[u] = p;
		q->to = u, q->cap = 0, q->flow = 0;
		q->nxt = adj[v], adj[v] = q;
		p->rev = q, q->rev = p;
		//printf("%d %d %d\n", u, v, c);
	}
	int aug(int x, int tot) {
		//printf("%d %d %d %d\n", x, s, t, tot);
		if( x == t ) return tot;
		int mind = t, sum = 0;
		for(edge *p=adj[x];p;p=p->nxt) {
			if( p->cap > p->flow ) {
				if( d[x] == d[p->to] + 1 ) {
					int del = aug(p->to, min(tot-sum, p->cap-p->flow));
					p->flow += del, p->rev->flow -= del, sum += del;
					if( sum == tot || vd[s] > t ) return sum; 
				}
				mind = min(mind, d[p->to]);
			}
		}
		if( !sum ) {
			vd[d[x]]--;
			if( !vd[d[x]] ) {
				d[s] = t + 1;
				return sum;
			}
			d[x] = mind + 1;
			vd[d[x]]++;
		}
		return sum;
	}
	int max_flow(int _s, int _t) {
		int flow = 0; s = _s, t = _t;
		while( d[s] <= t )
			flow += aug(s, INF);
		//printf("%d\n", flow);
		return flow;
	}
}G;
int deg[MAXV];
void solve() {
	G.init(); int m, s, ss, tt;
	scanf("%d%d", &m, &s);
	for(int i=1;i<=m;i++) deg[i] = 0;
	for(int i=1;i<=s;i++) {
		int x, y, d; scanf("%d%d%d", &x, &y, &d);
		if( d == 0 ) G.addedge(x, y, 1);
		deg[x]++, deg[y]--;
	}
	for(int i=1;i<=m;i++) {
		if( deg[i] % 2 ) {
			puts("impossible");
			return ;
		}
		deg[i] /= 2;
	}
	int tot = 0; ss = 0, tt = m + 1;
	for(int i=1;i<=m;i++)
		if( deg[i] > 0 ) G.addedge(ss, i, deg[i]), tot += deg[i];
		else if( deg[i] < 0 ) G.addedge(i, tt, -deg[i]);
	if( G.max_flow(ss, tt) == tot ) puts("possible");
	else puts("impossible");
}
int main() {
	int n; scanf("%d", &n);
	for(int i=1;i<=n;i++) solve();
}

codeforces 708D:Incorrect Flow
也是一道涉及预流,然后调整流使得其满足网络流性质的题。
这里可以戳到我的题解

posted @ 2019-02-15 16:50  Tiw_Air_OAO  阅读(562)  评论(0编辑  收藏  举报