//其實主要還是自己復習用
//假定讀者能夠熟練打dinic的板子
有上下界的網絡流的核心是”調整”,我們通過一個初始的未必可行的流調整出一個可行流,還可以從可行的未必最大/最小的流調整出最大/最小流.
另一個常用技巧是有源匯的流和無源匯的流(循環流)的轉換.除了無源匯可行流的求解,其他有源匯的上下界網絡流都要用到這個技巧.
- 無源匯有上下界可行流(也就是循環流)
模型:一個網絡,求出一個流,使得每條邊的流量必須>=Li且<=Hi,每個點必須滿足總流入量=總流出量(流量守恆)(這個流的特點是循環往復,無始無終).
這個算法是有上下界網絡流算法的基礎,只要深刻理解這個算法其他算法也就水到渠成,因此我用大篇幅力圖將這個算法的思想和細節闡述清楚.
可行流算法的核心是將一個不滿足流量守恆的初始流調整成滿足流量守恆的流.
流量守恆,即每個點的總流入量=總流出量
如果存在一個可行流,那么一定滿足每條邊的流量都大於等於流量的下限.因此我們可以令每條邊的流量等於流量下限,得到一個初始流,然后建出這個流的殘量網絡.(即:每條邊的流量等於這條邊的流量上限與流量下限之差)這個初始流不一定滿足流量守恆,因此最終的可行流一定是在這個初始流的基礎上增大了一些邊的流量使得所有點滿足流量守恆.
因此我們考慮在殘量網絡上求出一個另不滿足流量守恆的附加流,使得這個附加流和我們的初始流合並之后滿足流量守恆.即:
如果某個點在所有邊流量等於下界的初始流中滿足流量守恆,那么這個點在附加流中也滿足流量守恆,
如果某個點在初始流中的流入量比流出量多x,那么這個點在附加流中的流出量比流入量多x.
如果某個點在初始流中的流入量比流出量少x,那么這個點在附加流中的流出量比流入量少x.
可以認為附加流中一條從u到v的邊上的一個流量代表將原圖中u到v的流量增大1
X的數值可以枚舉x的所有連邊求出.比較方便的寫法是開一個數組A[],A[i]表示i在初始流中的流入量-流出量的值,那么A[i]的正負表示流入量和流出量的大小關系,下面就用A[i]表示初始流中i的流入量-流出量
但是dinic算法能夠求的是滿足流量守恆的有源匯最大流,不能在原網絡上直接求一個這樣的無源匯且不滿足流量守恆的附加流.注意到附加流是在原網絡上不滿足流量守恆的,這啟發我們添加一些原網絡之外的邊和點,用這些邊和點實現”原網絡上流量不守恆”的限制.
具體地,如果一個點i在原網絡上的附加流中需要滿足流入量>流出量(初始流中流入量<流出量,A[i]<0),那么我們需要給多的流入量找一個去處,因此我們建一條從i出發流量=-A[i]的邊.如果A[i]>0,也就是我們需要讓附加流中的流出量>流入量,我們需要讓多的流出量有一個來路,因此我們建一條指向i的流量=A[i]的邊.
當然,我們所新建的從i出發的邊也要有個去處,指向i的邊也要有個來路,因此我們新建一個虛擬源點ss和一個虛擬匯點tt(雙寫字母是為了和有源匯網絡流中的源點s匯點t相區分).新建的指向i的邊都從ss出發,從i出發的邊都指向tt.一個點要么有一條邊指向tt,要么有一條邊來自ss,
指向tt的邊的總流量上限一定等於ss流出的邊的總流量上限,因為每一條邊對兩個點的A[i]貢獻一正一負大小相等,所以全部點的A[i]之和等於0,即小於0的A[i]之和的絕對值=大於0的A[i]之和的絕對值.
如果我們能找到一個流滿足新加的邊都滿流,這個流在原圖上的部分就是我們需要的附加流(根據我們的建圖方式,“新加的邊都滿流”和”附加流合並上初始流得到流量平衡的流”是等價的約束條件).
那么怎樣找出一個新加的邊都滿流的流呢?可以發現假如存在這樣的方案,這樣的流一定是我們所建出的圖的ss-tt最大流,所以跑ss到tt的最大流即可.如果最大流的大小等於ss出發的所有邊的流量上限之和(此時指向tt的邊也一定滿流,因為這兩部分邊的流量上限之和相等).
最后,每條邊在可行流中的流量=容量下界+附加流中它的流量(即跑完dinic之后所加反向邊的權值).
代碼(ZOJ2314 Reactor Cooling)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=300,maxm=100000; struct edge{ int to,next,w,num; }lst[maxm];int len=0,first[maxn],_first[maxn]; void addedge(int a,int b,int w,int num){ lst[len].num=num; lst[len].to=b;lst[len].next=first[a];lst[len].w=w;first[a]=len++; lst[len].num=num; lst[len].to=a;lst[len].next=first[b];lst[len].w=0;first[b]=len++; } int vis[maxn],dis[maxn],q[maxn],head,tail,s,t,T; bool bfs(){ vis[s]=++T;dis[s]=1;head=tail=0;q[tail++]=s; while(head!=tail){ int x=q[head++]; for(int pt=first[x];pt!=-1;pt=lst[pt].next){ if(lst[pt].w&&vis[lst[pt].to]!=T){ vis[lst[pt].to]=T;dis[lst[pt].to]=dis[x]+1;q[tail++]=lst[pt].to; } } } if(vis[t]==T)memcpy(_first,first,sizeof(first)); return vis[t]==T; } int dfs(int x,int lim){ if(x==t){ return lim; } int flow=0,a; for(int pt=_first[x];pt!=-1;pt=lst[pt].next){ _first[x]=pt; if(lst[pt].w&&dis[lst[pt].to]==dis[x]+1&&(a=dfs(lst[pt].to,min(lst[pt].w,lim-flow)))){ lst[pt].w-=a;lst[pt^1].w+=a;flow+=a; if(flow==lim)return flow; } } return flow; } int dinic(){ int ans=0,x; while(bfs()) while(x=dfs(s,0x7f7f7f7f))ans+=x; return ans; } int low[maxm],ans[maxm]; int totflow[maxn],n,m; void work(){ memset(totflow,0,sizeof(totflow)); memset(first,-1,sizeof(first));len=0; scanf("%d%d",&n,&m); int u,v,b; s=0;t=n+1; for(int i=1;i<=m;++i){ scanf("%d%d%d%d",&u,&v,&low[i],&b); addedge(u,v,b-low[i],i);totflow[u]-=low[i];totflow[v]+=low[i]; } int sum=0; for(int i=1;i<=n;++i){ if(totflow[i]<0){ addedge(i,t,-totflow[i],0); }else{ sum+=totflow[i]; addedge(s,i,totflow[i],0); } } if(dinic()==sum){ puts("YES"); for(int i=1;i<=n;++i){ for(int pt=first[i];pt!=-1;pt=lst[pt].next){ if(lst[pt].num==0||pt%2==0)continue; ans[lst[pt].num]=lst[pt].w+low[lst[pt].num]; } } for(int i=1;i<=m;++i)printf("%d\n",ans[i]); }else puts("NO"); } int main(){ int tests;scanf("%d",&tests); while(tests--){ work();if(tests)printf("\n"); } return 0; }
2. 有源匯有上下界可行流
模型:現在的網絡有一個源點s和匯點t,求出一個流使得源點的總流出量等於匯點的總流入量,其他的點滿足流量守恆,而且每條邊的流量滿足上界和下界限制.
源點和匯點不滿足流量守恆,這讓我們很難辦,因此我們想辦法把問題轉化成容易處理的每個點都滿足流量守恆的無源匯情況.
為了使源匯點滿足流量守恆,我們需要有邊流入源點s,有邊流出匯點t.注意到源點s的流出量等於匯點t的流入量,我們就可以從匯點t向源點s連一條下界為0上界為無窮大的邊,相當於把從源點s流出的流量再流回來.在這樣的圖中套用上面的算法求出一個可行的循環流,拆掉從匯點t到源點s的邊就得到一個可行的有源匯流.
這里有一個小問題:最后得到的可行的有源匯流的流量是多少?
可以發現,循環流中一定滿足s流出的總流量=流入s的總流量,假定原圖中沒有邊流入s,那么s流出的流量就是t到s的無窮邊的流量,也就是s-t可行流的流量.因此我們最后看一下t到s的無窮邊的流量(即dinic跑完之后反向邊的權值)即可知道原圖中有源匯可行流的流量.
代碼:這個可行流算法在有源匯有上下界最大流/最小流中都會用到,可以看下面兩個算法的代碼
3.有源匯有上下界最大流
模型:現在的網絡有一個源點s和匯點t,求出一個流使得源點的總流出量等於匯點的總流入量,其他的點滿足流量守恆,而且每條邊的流量滿足上界和下界限制.在這些前提下要求總流量最大.
首先套用上面的算法求出一個有源匯有上下界可行流.此時的流不一定最大.
接下來在殘量網絡上跑s-t最大流即可.
最終的最大流流量=可行流流量(即t到s的無窮邊上跑出的流量)+新增廣出的s-t流量
問題:會不會增廣的時候使得一些邊不滿足流量下限?
不會.因為我們一開始建的圖就是把大小等於流量下限的流量拿出去之后的殘量網絡,這些流量根本沒有在圖中出現.
代碼:ZOJ 3229 Shoot The Bullet 東方文花帖 (由於ZOJ的評測插件似乎掛了,並不知道對不對,請謹慎取用)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=2005,maxm=100005; const int inf=0x7f7f7f7f; struct edge{ int to,next,w,num; }lst[maxm];int len=0,first[maxn],_first[maxn]; void addedge(int a,int b,int w,int num){ lst[len].num=num; lst[len].to=b;lst[len].next=first[a];lst[len].w=w;first[a]=len++; lst[len].num=num; lst[len].to=a;lst[len].next=first[b];lst[len].w=0;first[b]=len++; } int q[maxn],vis[maxn],dis[maxn],T,s,t,head,tail,ss,tt; bool bfs(){ head=tail=0;vis[s]=++T;q[tail++]=s; while(head!=tail){ int x=q[head++]; for(int pt=first[x];pt!=-1;pt=lst[pt].next){ if(lst[pt].w&&vis[lst[pt].to]!=T){ vis[lst[pt].to]=T;dis[lst[pt].to]=dis[x]+1;q[tail++]=lst[pt].to; } } } if(vis[t]==T)memcpy(_first,first,sizeof(first)); return vis[t]==T; } int dfs(int x,int lim){ if(x==t)return lim; int flow=0,a; for(int pt=_first[x];pt!=-1;pt=lst[pt].next){ _first[x]=pt; if(lst[pt].w&&dis[lst[pt].to]==dis[x]+1&&(a=dfs(lst[pt].to,min(lst[pt].w,lim-flow)))){ lst[pt].w-=a;lst[pt^1].w+=a;flow+=a; if(flow==lim)return flow; } } return flow; } int dinic(){ int ans=0,x; while(bfs()) while(x=dfs(s,inf))ans+=x; return ans; } int totflow[maxn]; void Add(int a,int b,int lo,int hi,int num){ totflow[a]-=lo;totflow[b]+=lo; addedge(a,b,hi-lo,num); } int low[maxm],ans[maxm]; int n,m,tot; void bound_flow(){ int sum=0; for(int i=s;i<=t;++i){ if(totflow[i]<0){ addedge(i,tt,-totflow[i],0); }else{ sum+=totflow[i]; addedge(ss,i,totflow[i],0); } } addedge(t,s,0x7f7f7f7f,0); int tmps=s,tmpt=t; s=ss;t=tt; if(dinic()==sum){ for(int pt=first[ss];pt!=-1;pt=lst[pt].next){ lst[pt].w=lst[pt^1].w=0; } for(int pt=first[tt];pt!=-1;pt=lst[pt].next){ lst[pt].w=lst[pt^1].w=0; } int flow0=lst[len-1].w; lst[len-1].w=lst[len-2].w=0; s=tmps;t=tmpt; printf("%d\n",flow0+dinic()); for(int i=1;i<=m;++i){ for(int pt=first[i+n];pt!=-1;pt=lst[pt].next){ if(lst[pt].num!=0){ ans[lst[pt].num]=lst[pt].w+low[lst[pt].num]; } } } for(int i=1;i<=tot;++i)printf("%d\n",ans[i]); }else{ printf("-1\n"); } } void work(){ s=0;t=n+m+1; ss=n+m+2;tt=n+m+3; memset(first,-1,sizeof(first));len=0; memset(totflow,0,sizeof(totflow)); int x,y; for(int i=1;i<=m;++i){ scanf("%d",&x); Add(n+i,t,x,inf,0); } int l,h; tot=0; for(int i=1;i<=n;++i){ scanf("%d%d",&x,&y); Add(s,i,0,y,0); for(int j=1;j<=x;++j){ ++tot; scanf("%d%d%d",&y,&l,&h); Add(i,n+y+1,l,h,tot);low[tot]=l; } } bound_flow();printf("\n"); } int main(){ while(scanf("%d%d",&n,&m)!=EOF)work(); return 0; }
4.有源匯有上下界最小流
模型:現在的網絡有一個源點s和匯點t,求出一個流使得源點的總流出量等於匯點的總流入量,其他的點滿足流量守恆,而且每條邊的流量滿足上界和下界限制.在這些前提下要求總流量最小.
依然是先跑出一個有源匯可行流.這時候的流也不一定是最小的.假如我們能在殘量網絡上找到一條s-t的路徑使得去掉這條路徑上的流量之后仍然滿足流量下限,我們就可以得到一個更小的流.好像我們並沒有什么算法可以”找到盡可能多的能夠去除流量的路徑”
這時候需要我們再理解一下dinic的反向邊.反向邊的流量增加等價於正向邊的的流量減少.因此我們在殘量網絡上找出t到s的流就相當於減小了s到t的流,因此我們在跑出可行流的殘量網絡上跑t-s最大流,用可行流的大小減去這一次t-s最大流的大小就是最小流的大小.(t-s最大流其實是盡量縮減s-t方向的流).
問題:會不會使流量縮減到不滿足流量下限?
不會.和有源匯有上下限的最大流一樣,我們之前從每條邊上拿出了大小等於流量下限的流量構成初始流,這些流量不在我們建出的圖中.最極端的情況是縮減到所有邊的流量等於流量下限,不會更小了.
代碼:bzoj2502 清理雪道
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=205,maxm=100005; struct edge{ int to,next,w; }lst[maxm];int len=0,first[maxn],_first[maxn]; void addedge(int a,int b,int w){//printf("Add %d %d\n",a,b); lst[len].to=b;lst[len].next=first[a];lst[len].w=w;first[a]=len++; lst[len].to=a;lst[len].next=first[b];lst[len].w=0;first[b]=len++; } int q[maxn],vis[maxn],dis[maxn],head,tail,s,t,T,ss,tt; bool bfs(){ head=tail=0;vis[s]=++T;dis[s]=1;q[tail++]=s; while(head!=tail){ int x=q[head++]; for(int pt=first[x];pt!=-1;pt=lst[pt].next){ if(lst[pt].w&&vis[lst[pt].to]!=T){ vis[lst[pt].to]=T;dis[lst[pt].to]=dis[x]+1;q[tail++]=lst[pt].to; } } } if(vis[t]==T)memcpy(_first,first,sizeof(first)); return vis[t]==T; } int dfs(int x,int lim){ if(x==t)return lim; int flow=0,a; for(int pt=_first[x];pt!=-1;pt=lst[pt].next){ _first[x]=pt; if(lst[pt].w&&dis[lst[pt].to]==dis[x]+1&&(a=dfs(lst[pt].to,min(lst[pt].w,lim-flow)))){ lst[pt].w-=a;lst[pt^1].w+=a;flow+=a; if(flow==lim)return flow; } } return flow; } int dinic(){ int ans=0,x; while(bfs()){ while(x=dfs(s,0x7f7f7f7f))ans+=x; } return ans; } int totflow[maxn]; void del(int x){ for(int pt=first[x];pt!=-1;pt=lst[pt].next)lst[pt].w=lst[pt^1].w=0; } int main(){ int n;scanf("%d",&n); int x,y; memset(first,-1,sizeof(first)); for(int i=1;i<=n;++i){ scanf("%d",&x); for(int j=1;j<=x;++j){ scanf("%d",&y); totflow[i]--;totflow[y]++; addedge(i,y,0x7f7f7f7f); } } s=0;t=n+1;ss=n+2,tt=n+3; for(int i=1;i<=n;++i){ addedge(s,i,0x7f7f7f7f); addedge(i,t,0x7f7f7f7f); } for(int i=1;i<=n;++i){ if(totflow[i]<0){ addedge(i,tt,-totflow[i]); }else{ addedge(ss,i,totflow[i]); } } addedge(t,s,0x7f7f7f7f); int tmps=s,tmpt=t; s=ss;t=tt; dinic(); int flow0=lst[len-1].w; lst[len-1].w=lst[len-2].w=0; del(ss);del(tt); s=tmpt;t=tmps; printf("%d\n",flow0-dinic()); return 0; }
5.有源匯有上下界費用流(待填坑,bzoj3876和Codeforces 708D,不過這兩道題都可以用費用流的技巧避開上下界網絡流)