Processing math: 100%

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


@0 - 参考资料@

menci 的博客
liu_runda 的博客

@1 - 问题引入@

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

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

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

@2 - 上下界可行流@

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

根据定义我们有 f(u,v)=l(u,v)+f(u,v),其中 0f(u,v)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) 一样的作用。

找出 sstt 的最大流即可,如果满流说明有一个可行解。此时边 (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),也可以合并成一条边。
具体的话,可以统计 ssu 的容量和 utt 的容量和,根据其正负以及大小来建边。

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

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

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

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

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

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

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

@4 - 上下界费用流@

(先不考虑负环的问题)

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

考虑求解有源汇的最小费用最大流。
ts 连容量 inf,费用 0 的边,跑最小费用最大流,再 st 求最小费用最大流。

考虑求解有源汇的最小费用流。
ts 连容量 inf,费用 0 的边,跑最小费用最大流,再 st 求最小费用流。

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

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

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

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

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

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

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

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

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

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

带源汇的话,一样是 ts 连容量 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 @   Tiw_Air_OAO  阅读(589)  评论(0编辑  收藏  举报
编辑推荐:
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
阅读排行:
· 手把手教你在本地部署DeepSeek R1,搭建web-ui ,建议收藏!
· 新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用
· Janus Pro:DeepSeek 开源革新,多模态 AI 的未来
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(三):用.NET IoT库
· 【非技术】说说2024年我都干了些啥
点击右上角即可分享
微信分享提示