一般的网络流模型中对边只有上界的限制,及流过该边的流量不超过上界。上下界网络流的不同之处在于,每条边还有一个下界,即流过该边的流量不少于下界。其实一般的网络流也可以看做下界为0。
上下界网络流的模型可以分为以下几种类型:
1.无源汇的上下界可行流:没有源点和汇点,要求一组满足流量限制的可行流。
2.有源汇的上下界最大流:有源点和汇点,求满足流量限制的最大流。
3.有源汇的上下界最小流:有源点和汇点,求满足流量限制的最小流。
4.上下界费用流:每条边有费用,在上面3中类型的基础上使得总费用最小。
无论是哪种类型,首先要解决的是下界的限制。
对于一条边u->v,下界为l,上界为r。我们可以将这条边变为上界为r-l,下界为0。相当于在可行流的基础上每条边的流量都减小了l。但是这样做了之后,流量却不守恒了。流入v的流量少了l,流出u的流量也少了l。这时我们设置超级源点S和超级汇点T,从S向v连一条流量为l的边,从u到T连一条流量为l的边,来平衡流量。我们称这样的边为附加边。
其实我更习惯这样链附加边:记录一个数组d,对于一条边u->v,下界为l,d[v]+=l,d[u]-=l。然后扫描所有点,如果d[i]>0说明i需要流入d[i]的流量,所以从S向i连一条流量为d[i]的边,如果d[i]<0说明i需要流出-d[i]的流量,所以从i向T连一条流量为-d[i]的边。这样边数会少一些,跑起来就快一些。
对于不同的类型还有不同的处理技巧:
按上面的方式建边后,跑一遍S到T的最大流。检查所有附加边是否满流,若满流则存在可行流,否则不存在。
若存在可行流,一组可行解就是每条边流过的流量加上流量下界。
一道例题:zoj2314
题目大意:给n个点,及m根管子,每根管子可单向运输液体,每时每刻每根管子流进来的物质要等于流出去的物质,m条管子组成一个循环体,里面流躺物质。并且满足每根管子一定的流量限制,范围为[Li,Ri].即要满足每时刻流进来的不能超过Ri,同时最小不能低于Li。问是否可行,可行则输出一组可行解。
思路:按上面的方式建边直接跑就行了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
using namespace std;
#define N 21000
struct edge{
int x,d,next;
}e[1000100];
int first[N],tot,du[N],down[N],dis[N],Q[N],cur[N];
int z,p,q,x,y,n,m,s,t;
void add(int x,int y,int z){
e[++tot].x=y;
e[tot].next=first[x];
e[tot].d=z;
first[x]=tot;
}
void init(){
scanf("%d%d",&n,&m);
tot=1; memset(du,0,sizeof(du));
memset(first,0,sizeof(first));
s=n+1; t=n+2;
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&x,&y,&down[i],&z);
add(x,y,z-down[i]); add(y,x,0);
du[x]-=down[i]; du[y]+=down[i];
}
for(int i=1;i<=n;i++)
if(du[i]<0) add(i,t,-du[i]),add(t,i,0);
else add(s,i,du[i]),add(i,s,0);
}
bool bfs(){
memset(dis,-1,sizeof(dis));
Q[p=q=1]=s; dis[s]=0;
for(;p<=q;p++){
int x=Q[p];
for(int i=first[x];i;i=e[i].next)
if(e[i].d&&dis[e[i].x]==-1){
dis[e[i].x]=dis[x]+1;
Q[++q]=e[i].x;
}
}
if(dis[t]==-1) return 0;
return 1;
}
int dfs(int x,int y){
if(x==t) return y;
int sum=0,tmp;
for(int i=cur[x];i;i=e[i].next)
if(e[i].d&&dis[e[i].x]==dis[x]+1){
tmp=dfs(e[i].x,min(e[i].d,y-sum));
sum+=tmp; e[i].d-=tmp; e[i^1].d+=tmp;
if(e[i].d) cur[x]=i;
if(sum==y) return sum;
}
if(sum==0) dis[x]=-1;
return sum;
}
int dinic(){
int ans=0;
while(bfs()){
for(int i=1;i<=t;i++) cur[i]=first[i];
ans+=dfs(s,1000000007);
}
for(int i=first[s];i;i=e[i].next)
if(e[i].d) return puts("NO"),ans;
puts("YES");
for(int i=2;i<=2*m;i+=2)
printf("%d\n",e[i+1].d+down[i/2]);
return ans;
}
int main(){
int T;
scanf("%d",&T);
while(T--){
init();
dinic();
putchar('\n');
}
return 0;
}
源点和汇点是不满足流量平衡的,不能直接套用上面的方法。我们可以先将汇点向源点连一条上界为正无穷,下界为0的边。将源点和汇点变为满足流量平衡的普通点,再按上面的方式建边后,跑一遍S到T的最大流。若附加边未满流则无解。
跑完一边后,我们将每条边的下界满足了,但这还不是最大流。因为这是原图中还有流量可流。我们将附加边和汇点向源点连的边删去,再跑一次最大流。这次就是真正的最大流了。
例题:zoj3229
题目大意:一个人给m个人拍照,计划拍照n天,每一天最多个C个人拍照,每天拍照数不能超过D张,而且给每个人i拍照有数量限制[Li,Ri],对于每个人n天的拍照总和不能少于Gi,如果有解求屌丝最多能拍多少张照,并求每天给对应女神拍多少张照;否则输出-1。
解题思路:增设一源点s,汇点t,s到第i天连一条上界为Di下界为0的边,每个人到汇点连一条下界为Gi上界为无穷的边,对于每一天,当天到第i个人连一条[Li,Ri]的边。按上面的方法构图就行了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
using namespace std;
#define INF 1000000007
struct edge{
int x,data,next,id;
}e[1000000];
int first[2000],dis[2000],du[2000],ans[100000];
int s,t,tot,ss,sum,num,tt,x,y,d,c,n,m;
void add(int x,int y,int d,int id){
e[++tot].x=y;
e[tot].data=d;
e[tot].id=id;
e[tot].next=first[x];
first[x]=tot;
if(~tot&1) add(y,x,0,id);
}
bool bfs(){
int c[2000],p,q;
memset(dis,-1,sizeof(dis));
dis[c[1]=s]=1;
for(p=q=1;p<=q;p++)
for(int i=first[c[p]];i;i=e[i].next)
if(e[i].data&&dis[e[i].x]==-1){
dis[e[i].x]=dis[c[p]]+1;
c[++q]=e[i].x;
}
return dis[t]!=-1;
}
int dfs(int x,int y){
if(x==t||y==0) return y;
int tmp,rec=0;
for(int i=first[x];i;i=e[i].next)
if(e[i].data&&dis[e[i].x]==dis[x]+1){
tmp=dfs(e[i].x,min(e[i].data,y-rec));
rec+=tmp; e[i].data-=tmp; e[i^1].data+=tmp;
if(rec==y) return rec;
}
if(rec==0) dis[x]=-1;
return rec;
}
int dinic(){
int ans=0;
while(bfs()) ans+=dfs(s,INF);
return ans;
}
void solve(){
ss=n+m+1; tt=n+m+2;
for(int i=1;i<=m;i++){
scanf("%d",&x);
du[i+n]-=x; du[tt]+=x;
add(i+n,tt,INF-x,0);
}
for(int i=1;i<=n;i++){
scanf("%d%d",&c,&d);
add(ss,i,d,0);
for(int j=1;j<=c;j++){
scanf("%d%d%d",&d,&x,&y);
du[i]-=x; du[n+d+1]+=x;
ans[++num]=x;
add(i,n+d+1,y-x,num);
}
}
s=n+m+3; t=n+m+4;
for(int i=1;i<=n+m+2;i++)
if(du[i]>0) add(s,i,du[i],0);
else add(i,t,-du[i],0);
add(tt,ss,INF,0);
dinic();
for(int i=first[s];i;i=e[i].next)
if(e[i].data) {puts("-1"); return;}
first[s]=first[t]=0;
s=ss; t=tt;
sum=dinic();
printf("%d\n",sum);
for(int i=3;i<=tot;i+=2)
ans[e[i].id]+=e[i].data;
for(int i=1;i<=num;i++) printf("%d\n",ans[i]);
}
int main(){
while(~scanf("%d%d",&n,&m)){
memset(ans,0,sizeof(ans));
memset(first,0,sizeof(first));
memset(du,0,sizeof(du));
tot=1; num=0;
solve();
puts("");
}
return 0;
}
和有源汇的上下界最大流的构图相同。但是跑完第一次最大流时的答案并不是最小流。应为他只是满足了网络的下界,而网络中可能存在环,这样部分的流量我们没有充分利用。
所有我们先不加汇点到源点的流量为正无穷的边,跑一遍最大流。这是是将原图中的环流满。连上汇点到源点的流量为正无穷的边后再跑一次。若所有附加边满流,则第二次的答案即为最小流,否则无解。
例题:sgu176
题目大意:有一个加工生产的机器,起点为1终点为n,中间生产环节有货物加工数量限制,输出u v z c,描述一个加工环节,当c等于1时表示这个加工的环节必须对纽带上的货物全部加工(即上下界都为z),c等于0表示加工上界为z,下界为0,起点最少需要投放多少货物才能传送带正常工作。
解题思路:按上面的方式构图即可。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
using namespace std;
#define N 110
#define INF 1000000007
struct edge{
int x,next,data,id;
}e[N*N];
int tot=1,first[N],sum[N],c,dis[N],f[N*N],ans,n,m,x,y,z,s,t;
queue<int>q;
void add(int x,int y,int z,int id){
e[++tot].x=y;
e[tot].data=z;
e[tot].id=id;
e[tot].next=first[x];
first[x]=tot;
}
bool bfs(){
memset(dis,-1,sizeof(dis));
q.push(s); dis[s]=1;
while(!q.empty()){
x=q.front(); q.pop();
for(int i=first[x];i;i=e[i].next)
if(e[i].data&&dis[e[i].x]==-1){
dis[e[i].x]=dis[x]+1;
q.push(e[i].x);
}
}
return dis[t]!=-1;
}
int dfs(int x,int y){
if(x==t||y==0) return y;
int tmp,rec=0;
for(int i=first[x];i;i=e[i].next)
if(dis[e[i].x]==dis[x]+1&&e[i].data){
tmp=dfs(e[i].x,min(y-rec,e[i].data));
rec+=tmp; e[i].data-=tmp; e[i^1].data+=tmp;
if(rec==y) return rec;
}
if(rec==0) dis[x]=-1;
return rec;
}
int dinic(){
int ans=0;
while(bfs())
ans+=dfs(s,INF);
return ans;
}
void work(){
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&x,&y,&c,&z);
if(z) sum[x]-=c,sum[y]+=c,f[i]=c;
else add(x,y,c,i),add(y,x,0,i);
}
s=n+1; t=n+2;
for(int i=1;i<=n;i++)
if(sum[i]>0) add(s,i,sum[i],0),add(i,s,0,0);
else add(i,t,-sum[i],0),add(t,i,0,0);
dinic();
add(n,1,INF,0); add(1,n,0,0);
ans=dinic();
for(int i=first[s];i;i=e[i].next)
if(e[i].data){puts("Impossible");return;}
printf("%d\n",ans);
for(int i=3;i<=tot;i+=2) f[e[i].id]=e[i].data;
for(int i=1;i<m;i++) printf("%d ",f[i]);
printf("%d\n",f[m]);
}
int main(){
while(~scanf("%d%d",&n,&m)){
memset(sum,0,sizeof(sum));
memset(first,0,sizeof(first));
tot=1; work();
}
return 0;
}
先认准是上面模型三种中的哪一种,按其构图方法构图。对于原图中的边,费用为该边的费用,对于附加边,费用为0。将最大流改为费用流即可。但是答案还要加上所有边的费用乘上该边的流量下界。应为我们算出来的费用是没有计算下界的费用的。
例题:3876: [Ahoi2014]支线剧情
题目大意:一张有向无环图,每条边有费用。从任意点都可回到起点,无需费用。求由起点遍历所有边的最小费用。
解题思路:每条边必须经过一次,便将边的上界设为正无穷,下界设为1。任何点可以回到起点,便将所有点向起点连一条上界为正无穷,下界为0,费用为0的边。整个图变成了一个无源汇的图。按无源汇的可行流构图,跑费用流就可以了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
using namespace std;
#define N 400
#define INF 1000000007
int first[N],dis[N],c[N*N],pre[N],du[N];
int p,q,n,s,t,k,x,y,tot=1,sum;
bool v[N];
struct edge{
int from,to,data,cost,next;
}e[N*N];
void add(int x,int y,int d,int c){
e[++tot].to=y;
e[tot].from=x;
e[tot].data=d;
e[tot].cost=c;
e[tot].next=first[x];
first[x]=tot;
}
bool spfa(){
memset(dis,63,sizeof(dis));
dis[c[1]=s]=0; v[s]=true;
for(p=q=1;p<=q;v[c[p]]=false,p++)
for(int i=first[c[p]];i;i=e[i].next)
if(e[i].data&&dis[e[i].to]>dis[c[p]]+e[i].cost){
dis[e[i].to]=dis[c[p]]+e[i].cost;
pre[e[i].to]=i;
if(!v[e[i].to]) v[c[++q]=e[i].to]=true;
}
return dis[t]!=dis[0];
}
int costflow(){
int ans=0,flow;
while(spfa()){
flow=INF;
for(int i=pre[t];i;i=pre[e[i].from])
flow=min(flow,e[i].data);
for(int i=pre[t];i;i=pre[e[i].from]){
ans+=flow*e[i].cost;
e[i].data-=flow; e[i^1].data+=flow;
}
}
return ans;
}
int main(){
scanf("%d",&n);
s=n+1; t=n+2;
for(int i=1;i<=n;i++){
scanf("%d",&k);
for(int j=1;j<=k;j++){
scanf("%d%d",&x,&y);
add(i,x,INF,y); add(x,i,0,-y);
du[x]++; du[i]--;
sum+=y;
}
if(i!=1) add(i,1,INF,0),add(1,i,0,0);
}
for(int i=1;i<=n;i++)
if(du[i]>0) add(s,i,du[i],0),add(i,s,0,0);
else add(i,t,-du[i],0),add(t,i,0,0);
printf("%d\n",costflow()+sum);
return 0;
}