上下界网络流

一:概述

一般的网络流模型中对边只有上界的限制,及流过该边的流量不超过上界。上下界网络流的不同之处在于,每条边还有一个下界,即流过该边的流量不少于下界。其实一般的网络流也可以看做下界为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]的边。这样边数会少一些,跑起来就快一些。

对于不同的类型还有不同的处理技巧:

1.无源汇的上下界可行流

按上面的方式建边后,跑一遍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;
}

2.有源汇的上下界最大流

源点和汇点是不满足流量平衡的,不能直接套用上面的方法。我们可以先将汇点向源点连一条上界为正无穷,下界为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;
}

有源汇的上下界最小流

和有源汇的上下界最大流的构图相同。但是跑完第一次最大流时的答案并不是最小流。应为他只是满足了网络的下界,而网络中可能存在环,这样部分的流量我们没有充分利用。
比如下面这张图:这样求得的最小流为200,而实际的可行最小流解只需100。
这里写图片描述
所有我们先不加汇点到源点的流量为正无穷的边,跑一遍最大流。这是是将原图中的环流满。连上汇点到源点的流量为正无穷的边后再跑一次。若所有附加边满流,则第二次的答案即为最小流,否则无解。

例题: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;
}

4.上下界费用流

先认准是上面模型三种中的哪一种,按其构图方法构图。对于原图中的边,费用为该边的费用,对于附加边,费用为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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值