有上下界的网络流问题

前几天就想写了的,一直没写,今天就写完吧。

因为在做这些上下界的题的时候,遇到了很多问题,在大神的帮助下还是一一解决了的。(英文没学好诶喂,,在sgu和poj各种wa。。)

主要是没看题,求上下界已经理解了的。。

分3种上下界网络流问题:(在本文只说做法和一些相关的东西,证明和推导请看后面写出的参考)

无源无汇的上下界最大流问题(没有源和汇我的理解,即流量只需满足流的3个性质即可,没有从源点流出从汇流进的约束):

做法:将图的下界分离到一个附加源和汇中,而上界则变为原弧的上界减去下界的差,构成一个附加网络,再在附加源汇上跑一次最大流即可。其中,记录每个点的入流下界和-出流下界和,当下界和>0时怎连入一条源s到这个点的边,上界为这个下界和,下界为0;当下界和<0则连入一条这个点到汇t的边,上界为下界和的绝对值,下界为0。

判断:仅当以源s为起点的弧或以汇t为终点的弧全部流满,则满足下界的可行流才存在,否则不存在。

例题:sgu194

代码:

#include <cstdio>
#include <cstring>
using namespace std;
#define CC(a, c) memset(a, c, sizeof(a))
#define FOR(i,a,n) for(i=a;i<=n;++i)

const int maxn=500, oo=~0u>>1, maxm=80810;
int S, T, cap[maxm], d[maxn], cur[maxn], gap[maxn], p[maxn];
int N, ihead[maxn], inext[maxm], to[maxm], from[maxm], cnt;
int dn[maxm], du[maxn];

int min(const int& a, const int& b) { return a < b ? a : b; }

int isap() {
	int flow=0, u, v, i, f=oo;
	CC(d, 0); CC(gap, 0);
	for(i=1; i<=N; ++i) cur[i]=ihead[i];
	u=S; gap[0]=N;
	while(d[S]<N) {
		for(i=cur[u]; i; i=inext[i]) if(cap[i] && d[u]==d[to[i]]+1) break;
		if(i) {
			cur[u]=i; v=to[i]; f=min(f, cap[i]); p[v]=i; u=v;
			if(v==T) {
				for(; v!=S; v=from[p[v]]) cap[p[v]]-=f, cap[p[v]^1]+=f;
				u=S; flow+=f; f=oo;
			}
		}
		else {
			if( !(--gap[d[u]]) ) break;
				d[u]=N;
				cur[u]=ihead[u];
				for(i=cur[u]; i; i=inext[i]) if(cap[i] && d[u]>d[to[i]]+1) d[u]=d[to[i]]+1;
				gap[d[u]]++; if(u!=S) u=from[p[u]];
			}
		}
	return flow;
}

void add(int u, int v, int c) {
	inext[++cnt]=ihead[u]; ihead[u]=cnt; from[cnt]=u; to[cnt]=v; cap[cnt]=c;
	inext[++cnt]=ihead[v]; ihead[v]=cnt; from[cnt]=v; to[cnt]=u; cap[cnt]=0;
}

void init(int s, int t, int n) {
	S=s; T=t; N=n; cnt=1;
	CC(ihead, 0); CC(du, 0); CC(dn, 0);
}

int main() {
	int n, m, u, v, down, up, i, flag;
	while(~scanf("%d%d", &n, &m)) {
		init(n+1, n+2, n+2);
		for(i=1; i<=m; ++i) {
			scanf("%d%d%d%d", &u, &v, &down, &up);
			add(u, v, up-down);
			du[u]-=down; du[v]+=down; //入流就+,出流就-
			dn[i]=down; //记录下界,因为最后这些边上的流量是不包括下界的,因此最后要加回来
		}
		for(i=1; i<=n; ++i) {
			if(du[i]>0) add(S,i,du[i]); //>0就从源s连一条弧
			if(du[i]<0) add(i,T,-du[i]); //<0就连一条弧到汇t
		}
		isap();
		flag=1;
		for(i=ihead[S]; i; i=inext[i]) if(cap[i]) { flag=0; break; } //仅当流满才存在
		if(!flag) puts("NO");
		else {
			puts("YES");
			for(i=1; i<=m; ++i)
			printf("%d
",cap[(i<<1)+1]+dn[i]); //加回下界
		}
	}
	return 0;
}

有源汇的上下界最大流问题:

做法:从汇t连一条弧到源s,上界为无限大,下界为0。再设一个超级源ss和一个超级汇tt,按照无源汇上下界最大流的方法连边给ss和tt,从ss跑最大流到tt,判断是否有弧满后,去掉超级源和汇及其边,跑一次s-t的最大流(注意这里最大流绝不是t-s的反向弧,因为在第二次跑最大流的时候将这弧退掉了),最后根据题目要求输出即可。

技巧:我用的是链式前向星(邻接表),因此将ss和tt的head去掉即可,为什么呢,因为这不会影响到后面跑最大流,因为ss和tt本来就不在s-t的增广路上,但这里要注意,因为ss和tt还在图中,因此调用最大流的算法时,点的数量要包括了ss和tt。

例题:zoj3229

ps:这题是我调试了2天的。。太坑了,很多细节,还是注意看题吧。。比如说按输入的顺序输出。。导致我算法没错则一直在找算法的错误,还是请大神帮我找出来的。。。。。但是还是找到了一些bug且大神帮我优化了我的sap的一个地方,还是很感谢他的~

ps:这题样例是有问题的,请不要相信第一个case,用第二个case来做测试吧

代码:

#include <cstdio>
#include <cstring>
using namespace std;
#define CC(a, c) memset(a, c, sizeof(a))
#define FOR(i,a,n) for(i=a;i<=n;++i)

const int maxn=1510, oo=100000000, maxm=151000;
int cap[maxm], d[maxn], cur[maxn], gap[maxn], p[maxn];
int ihead[maxn], inext[maxm], to[maxm], from[maxm], cnt;
int inout[maxn], down[400][1200], id[400][1200]; //这里用maxn*maxn的话就mle了。。
int E[maxm][2], e;

int min(const int& a, const int& b) { return a < b ? a : b; }

int isap(int s, int t, int n) {
	int flow=0, i, u, f=oo;
	CC(gap, 0); CC(d, 0);
	FOR(i, 0, n) cur[i]=ihead[i];
	u=s; gap[0]=n;
	while(d[s]<n) {
		for(i=cur[u]; i; i=inext[i]) if(cap[i] && d[u]==d[to[i]]+1) break;
		if(i) {
			cur[u]=i; p[to[i]]=i; u=to[i];
			if(u==t) {
				for(f=oo; u!=s; u=from[p[u]]) f=min(f, cap[p[u]]); //这里的优化是大神教的
				for(u=t; u!=s; u=from[p[u]]) cap[p[u]]-=f, cap[p[u]^1]+=f;
				flow+=f;
			}
		}
		else {
			if( !(--gap[d[u]]) ) break;
			d[u]=n;
			cur[u]=ihead[u];
			for(i=cur[u]; i; i=inext[i]) if(cap[i] && d[u]>d[to[i]]+1)
				d[u]=d[to[i]]+1;
			gap[d[u]]++; if(u!=s) u=from[p[u]];
		}
	}
	return flow;
}

void add(int u, int v, int c) {
	inext[++cnt]=ihead[u]; ihead[u]=cnt; from[cnt]=u; to[cnt]=v; cap[cnt]=c;
	inext[++cnt]=ihead[v]; ihead[v]=cnt; from[cnt]=v; to[cnt]=u; cap[cnt]=0;
}

void init() {
	CC(ihead, 0); cnt=1; CC(down, 0); CC(id, 0); CC(inout, 0);
	e=0;
}

int main() {
	int n, m, i, g, d, t, l, r, sum, S, T;
	while(~scanf("%d%d", &n, &m)) {
		init();
		S=n+m+1; T=n+m+2;
		FOR(i, 1, m) {
			scanf("%d", &g);
			add(i+n, T, oo-g);
			inout[i+n]-=g; inout[T]+=g;
		}
		FOR(i, 1, n) {
			scanf("%d%d", &g, &d);
			add(S, i, d); 
			while(g--) {
				scanf("%d%d%d", &t, &l, &r);
				inout[i]-=l; inout[t+1+n]+=l;
				add(i, t+1+n, r-l);
				down[i][t+1]+=l; id[i][t+1]=cnt; //记录下标,反向弧,因为用的是残量网络跑的最大流,所以最后应该是反向弧
				E[++e][0]=i; E[e][1]=t+1; //记录顺序好输出
			}
		}
		add(T, S, oo); //添加汇到源的oo弧
		S=n+m+3; T=n+m+4; //设置超级源和汇
		sum=0;
		FOR(i, 1, n+m+2) {
			if(inout[i]>0) { sum+=inout[i]; add(S, i, inout[i]); } //sum是判断满流的一种方法
			if(inout[i]<0) add(i, T, -inout[i]);
		}
		if(isap(S, T, T)!=sum) printf("-1
");
		else {
			ihead[S]=ihead[T]=0; //去掉超级源和汇的弧,注意,这里只是删掉点,也就是说,反向弧还存在。但是反向弧不会影响最大流,因为它不在增广路上面
			S=n+m+1; T=n+m+2; //恢复源和汇
			printf("%d
", isap(S, T, T+2)); //打印最大流,这里要注意,因为点集里还有超级源和汇,所以N要包括那2个点
			FOR(i, 1, e)
				printf("%d
", down[ E[i][0] ][ E[i][1] ]+cap[id[ E[i][0] ][ E[i][1]] ]);
		}
		printf("
");
	} 
	return 0;
}
	

有源汇的上下界最小流问题:

做法:先不连边t-s,先构造了附加网络到超级源和超级汇中,然后跑超级源到超级汇的最大流,再连汇t到源s上界为无限大下界为0的弧,再跑一次超级源到超级汇的最大流即可,最小流就是t-s的反向弧。。。(ps,这里我不明白为什么这样做,问了之前帮我的大神,他也不知道,因此我先放下吧,以后问问其它大神。。其实我有一个理解,就是先将之前 上界-下界 的那些弧的最大跑出来, 然后连边后再跑,其实就是退流,将之前跑的退回去,这样既满足了下界限制,又是可行流,因为退流回去的是最大流,因此第二次跑出来的是最小流)

例题:sgu176

代码:

#include <cstdio>
#include <cstring>
using namespace std;
#define CC(a, c) memset(a, c, sizeof(a))
#define FOR(i,a,n) for(i=a;i<=n;++i)

const int maxn=1510, oo=100000000, maxm=151000;
int cap[maxm], d[maxn], cur[maxn], gap[maxn], p[maxn];
int ihead[maxn], inext[maxm], to[maxm], from[maxm], cnt;
int inout[maxn], ans[maxm], id[maxm];

int min(const int& a, const int& b) { return a < b ? a : b; }

int isap(int s, int t, int n) {
	int flow=0, i, u, f=oo;
	CC(gap, 0); CC(d, 0);
	FOR(i, 0, n) cur[i]=ihead[i];
	u=s; gap[0]=n;
	while(d[s]<n) {
		for(i=cur[u]; i; i=inext[i]) if(cap[i] && d[u]==d[to[i]]+1) break;
		if(i) {
			cur[u]=i; p[to[i]]=i; u=to[i];
			if(u==t) {
				for(f=oo; u!=s; u=from[p[u]]) f=min(f, cap[p[u]]);
				for(u=t; u!=s; u=from[p[u]]) cap[p[u]]-=f, cap[p[u]^1]+=f;
				flow+=f;
			}
		}
		else {
			if( !(--gap[d[u]]) ) break;
			d[u]=n;
			cur[u]=ihead[u];
			for(i=cur[u]; i; i=inext[i]) if(cap[i] && d[u]>d[to[i]]+1)
				d[u]=d[to[i]]+1;
			gap[d[u]]++; if(u!=s) u=from[p[u]];
		}
	}
	return flow;
}

void add(int u, int v, int c, int _id) {
	inext[++cnt]=ihead[u]; ihead[u]=cnt; from[cnt]=u; to[cnt]=v; cap[cnt]=c; id[cnt]=_id;
	inext[++cnt]=ihead[v]; ihead[v]=cnt; from[cnt]=v; to[cnt]=u; cap[cnt]=0; id[cnt]=0;
}

void init() {
	CC(ihead, 0); cnt=1; CC(ans, 0); CC(id, 0); CC(inout, 0);
}

int main() {
	int n, m, i, S, T, E, ss, tt;
	while(~scanf("%d%d", &n, &m)) {
		init();
		S=1; T=n;
		FOR(i, 1, m) {
			int u, v, c, k;
			scanf("%d%d%d%d", &u, &v, &c, &k);
			if(k) inout[u]-=c, inout[v]+=c, ans[i]=c;
			else add(u, v, c, i);
		}
		E=cnt;
		ss=n+1; tt=ss+1;
		FOR(i, 1, n) {
			if(inout[i]>0) add(ss, i, inout[i], 0);
			if(inout[i]<0) add(i, tt, -inout[i], 0);
		}
		isap(ss, tt, tt); //先跑一次最大流
		add(T, S, oo, 0); //连边
		isap(ss, tt, tt); //再跑一次最大流
		for(i=ihead[ss]; i; i=inext[i]) if(cap[i]) break;
		if(i) puts("Impossible");
		else {
			int res=0;
			for(i=ihead[T]; i; i=inext[i]) if(to[i]==S) break;
			res=cap[i^1];
			printf("%d
", res);
			FOR(i, 2, E) ans[id[i]]=cap[i^1];
			FOR(i, 1, m) if(i!=m) printf("%d ", ans[i]);
				else printf("%d
", ans[i]);
		}
	} 
	return 0;
}
	

参考:

周源《一种简易的方法求解流量有上下界的网络中网络流问题》

Mr. Ant博客:http://www.cnblogs.com/kane0526/archive/2013/04/05/3001108.html

原文地址:https://www.cnblogs.com/iwtwiioi/p/3826483.html