写在前面

[down,up]的范围内。

[0,cap]。

分类

有上下界的网络流分为两种:

  • 有源汇
    所有点都要求满足流量平衡
  • 无源汇
    除了源点汇点都要满足流量平衡(源点只有流出,汇点只有流入)

无源汇

可行流

down,可以直接将边的流量设为up−down。

down,会使得某些点不满足流量平衡。

这该怎么解决呢?

附加源和附加汇,来补充和吸收这些流量。

具体来说就是:

有上下界的网络流_#include


有上下界的网络流_#include_02


有上下界的网络流_网络流_03


from 《图论原理 胡伯涛》

因此,对于一条边u->v,我们在新图中连三条边:

  • u->v cap=up−down
  • S->v cap=down
  • u->T cap=down

∑downi,如果等于就是可行流,否则没有可行流。

*注:由于是无源汇的,所以并没有最大流最小流之说。

有源汇

可行流

[0,inf]的边,有源汇就变成了无源汇,像上面一样求解即可。

最小流

  • <法一>
    好理解,时间复杂度较高。
  • 因为s->t的流量与t->s的流量是相等的,我们可以通过限制t->s的流量来确定此时网络中的流量。
  • 因此使用二分来求解
  • 当前的二分范围是[l,r],mid=l+r2;如果t->s的流量设为mid,存在可行流,那么缩小上限即r=mid
  • <法二>
    较难理解,时间复杂度低。
  • 先不要连接t->s流量为inf的边,求一次最大流
  • inf的边,在残量网络上求一次最大流
  • 为什么这样做呢?
  • 感性的来理解,第一步中求最大流,所有能流的边都“竭尽全力”的流完了;
    第二步再求最大流的时候,t->s上的流量就会尽可能的小(即s->t的流量尽可能小)

最大流

  • <法一>
    与最小流中的二分法类似,只是变成了可行就调高下限。
  • <法二>
  • 连上t->s流量为inf的边,求一次附加源到附加汇的最大流
  • 在残量网络上求一次s到t的最大流
  • 最后答案就是第二次求出的最大流(因为第一次求出的最大流是t->s的流量,而这个流量在他的反向边s->t中出现,所以不需要另外加上)
  • 为什么这样做呢?
  • 依然感性的理解,第一次求出的(附加源到附加汇的)最大流是为了满足down尽可能流满,然而此时s->t上可能还有可行的流,我们在残量网络上继续来求最大流,可以使得最后求出的流既满足≥down的限制,且最大。

另一篇博客,写的也很好:​​javascript:void(0)​

调整”,我们通过一个初始的未必可行的流调整出一个可行流,还可以从可行的未必最大/最小的流调整出最大/最小流.

有源汇的流和无源汇的流(循环流)的转换.除了无源汇可行流的求解,其他有源汇的上下界网络流都要用到这个技巧.

  1. 无源汇有上下界可行流(也就是循环流)

模型:一个网络,求出一个流,使得每条边的流量必须>=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]的边.如果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;
}

有上下界的网络流_网络流_04


  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最大流即可.

最终的最大流流量=可行流流量(即t到s的无穷边上跑出的流量)+新增广出的s-t流量

问题:会不会增广的时候使得一些边不满足流量下限?

不会.因为我们一开始建的图就是把大小等于流量下限的流量拿出去之后的残量网络,这些流量根本没有在图中出现.

 东方文花帖


#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的路径使得去掉这条路径上的流量之后仍然满足流量下限,我们就可以得到一个更小的流.好像我们并没有什么算法可以”找到尽可能多的能够去除流量的路径”

这时候需要我们再理解一下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;
}