有上下界的网络流问题
前几天就想写了的,一直没写,今天就写完吧。
因为在做这些上下界的题的时候,遇到了很多问题,在大神的帮助下还是一一解决了的。(英文没学好诶喂,,在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\n",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\n"); else { ihead[S]=ihead[T]=0; //去掉超级源和汇的弧,注意,这里只是删掉点,也就是说,反向弧还存在。但是反向弧不会影响最大流,因为它不在增广路上面 S=n+m+1; T=n+m+2; //恢复源和汇 printf("%d\n", isap(S, T, T+2)); //打印最大流,这里要注意,因为点集里还有超级源和汇,所以N要包括那2个点 FOR(i, 1, e) printf("%d\n", down[ E[i][0] ][ E[i][1] ]+cap[id[ E[i][0] ][ E[i][1]] ]); } printf("\n"); } 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\n", 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\n", ans[i]); } } return 0; }
参考:
周源《一种简易的方法求解流量有上下界的网络中网络流问题》
Mr. Ant博客:http://www.cnblogs.com/kane0526/archive/2013/04/05/3001108.html