【无源汇有上下界的可行流】
首先定义 f (u,i)是从u到 i 的流量,b()是这条边的下限,c()是这条边的上限,g() 是可增加的部分。
我们将每一条边的流量分为两个部分,f(u,i) = b(u,i) + g(u,i),那么显然 c(u,i )>=b( u , i)+g (u,i )【*】
又根据流量平衡定理 sigma 【 b(u,i) + g(u,i)】=sigma 【b(i,v) + g(i,v)】
再移项得到 sigma b(u,i) - sigma b(i,v) =sigma g(i,v) - sigma g(u,i)
左边的是一个定值,我们看做 m(i)
则 m(i)= sigma g(i,v) - sigma g(u,i)
若m(i)> 0 则说明 u ~ i 这条边的下限比 i ~ v 的下限要大,那么u i 可以松弛的 g (u,i)就应该小于 i v 可以松弛的 g (i,v),这样流量才可能平衡咯
基于这条性质,我们建立一个超级源点 ss,超级汇点 tt,对于每一个点而言我们计算它的m(i),若大于0就从 ss 往 i 建一条边权为m(i)的边,若小于0就从 i 往 tt 建一条边权为 - m(i)的边
原来各个点之间的边值就相当于可以松弛的g (u,i),那么根据【*】这个式子可知, g 最多可以取到 c(u,i )- b( u , i),所以两点之间的容量(可以流过的最大值)=c(u,i )- b( u , i),然后在 ss 到 tt 之间跑一个最大流,如果从 ss 出来的每一条边都满流了,说明满足了所有下限,我们就算找到一条可行流(也可以判断最后跑出来的maxflow是否等于sigma m(i)【只取m(i)>0的情况】,我的代码就是这样的)
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<iostream>
#include<queue>
#define N 205
#define M 24000
#define inf 0x7fffffff
#define in read()//为了让代码好看一点我把读优省略了
#define debug(x) cerr<<#x<<'='<<x<<endl//这个东西很玄妙我在文章的最后讲一下
using namespace std;
int n,m,ss,tt;
int nxt[M],head[N],to[M],cap[M],cnt=1,end[N];
int cur[M],lev[M],low[M];
void add(int x,int y,int l,int r){
nxt[++cnt]=head[x];head[x]=cnt;to[cnt]=y;cap[cnt]=r-l;//建的是可松弛的边
low[cnt]=l;//记录下每一条边的下限
nxt[++cnt]=head[y];head[y]=cnt;to[cnt]=x;cap[cnt]=0;
end[y]+=l;end[x]-=l;//因为最后是sigma b(u,i) - sigma b(i,v)这样的,我们就直接在以 i 结尾的时候加上l,以 i 开始的时候减去 l ,就等价于这个式子了
}
bool bfs(){
memset(lev,-1,sizeof(lev));
memcpy(cur,head,sizeof(head));//当前弧优化操作
queue<int > q;//如果定义在外面则每次都需要清空
q.push(ss);lev[ss]=0;
while(!q.empty()){
int u=q.front();q.pop();
for(int e=head[u];e;e=nxt[e]){
int v=to[e];
if(cap[e]>0&&lev[v]==-1){
lev[v]=lev[u]+1;
q.push(v);
if(v==tt) return true;
}
}
}
return false;
}
int dinic(int u,int flow){
if(u==tt) return flow;
int res=0;
for(int &e=cur[u];e;e=nxt[e]){
int v=to[e];
if(cap[e]>0&&lev[v]>lev[u]){
int delta=dinic(v,min(flow-res,cap[e]));
if(delta){
cap[e]-=delta;cap[e^1]+=delta;
res+=delta;if(res==flow) break;
}
}
}
if(res<=flow) lev[u]=-1;
return res;
}
int main(){
n=in;m=in;
int i,j,k;
ss=0;tt=n+1;
for(i=1;i<=m;++i){
int s,t,ll,rr;
s=in;t=in;ll=in;rr=in;
add(s,t,ll,rr);
}
int sum=0;
for(i=1;i<=n;++i){
int hh=end[i];
if(hh<0) add(i,tt,0,-hh);
else add(ss,i,0,hh),sum+=hh;//判断是否存在可行流就看下限流满没有
}
int maxflow=0;
while(bfs())
maxflow+=dinic(ss,inf);//从ss到tt跑最大流
if(maxflow==sum){
printf("YES\n");
for(i=2;i<=2*m;i+=2){
printf("%d\n",low[i]+cap[i+1]);//反向边的流量就是这条边在跑最大流的时候流过去了多少加上下限,就是这条边实际流量
}
}
else printf("NO\n");
return 0;
}
【有源汇有上下界的最大流】
刚刚讲了无源汇的情况,那么有源汇怎么办呢?
……………………思考…………………………
好啦,聪明的你应该想到啦,我们直接从 t 到 s 连一条(0,inf) 的边,不就变成无源汇的情况啦
然后我们来想想如何求最大流,由于我们照之前的方法找到的只是一条原图中的可行流,而这个时候是满足所有的下限的,我们就只能在可松弛的边上下手,这不就又很容易了吗,我们去掉超级源汇点,然后在 t 到 s 连了一条(0,inf) 的边的图上再从s 到 t 跑最大流,最后跑出来的maxflow就是答案了
为什么呢?仔细思考当我们第一遍跑完最大流后,可行流的流量全都在(0,inf)这条边的反向边上,然后我们跑第二遍最大流,相当于就把可行流的流量退了回去,退到答案里,再增广其他路得到更大的可能
去掉源汇有两种做法:
- 把和源汇相连的边其cap全赋值为0(下一个代码使用方法1)
- 用一个vis数组标记(本代码使用方法2)
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<iostream>
#include<queue>
#define N 205
#define M 24000
#define inf 0x7fffffff
#define in read()
#define debug(x) cerr<<#x<<'='<<x<<endl
using namespace std;
int n,m,ss,tt,s,t;
int nxt[M],head[N],to[M],cap[M],cnt=1,end[N];
int cur[M],lev[M],low[M];
bool vis[N];
void add(int x,int y,int l,int r){
nxt[++cnt]=head[x];head[x]=cnt;to[cnt]=y;cap[cnt]=r-l;
low[cnt]=l;//记录下每一条边的下限
nxt[++cnt]=head[y];head[y]=cnt;to[cnt]=x;cap[cnt]=0;
end[y]+=l;end[x]-=l;
}
bool bfs(int hh,int ee){
memset(lev,-1,sizeof(lev));
memcpy(cur,head,sizeof(head));
queue<int > q;////////
q.push(hh);lev[hh]=0;
while(!q.empty()){
int u=q.front();q.pop();
for(int e=head[u];e;e=nxt[e]){
int v=to[e];
if(cap[e]>0&&lev[v]==-1&&vis[v]==0){
lev[v]=lev[u]+1;
q.push(v);
if(v==ee) return true;
}
}
}
return false;
}
int dinic(int u,int flow,int ee){
if(u==ee) return flow;
int res=0;
for(int &e=cur[u];e;e=nxt[e]){
int v=to[e];
if(cap[e]>0&&lev[v]>lev[u]&&vis[v]==0){
int delta=dinic(v,min(flow-res,cap[e]),ee);
if(delta){
cap[e]-=delta;cap[e^1]+=delta;
res+=delta;if(res==flow) break;
}
}
}
if(res<=flow) lev[u]=-1;
return res;
}
int read(){
char ch;
int f=1,res=0;
while((ch=getchar())<'0'||ch>'9')
if(ch=='-') f=-1;
while(ch>='0'&&ch<='9'){
res=(res<<3)+(res<<1)+ch-'0';
ch=getchar();
}
return f==1?res:-res;
}
int main(){
n=in;m=in;s=in;t=in;
int i,j,k;
ss=0;tt=n+1;
for(i=1;i<=m;++i){
int u,v,ll,rr;
u=in;v=in;ll=in;rr=in;
add(u,v,ll,rr);
}
int sum=0;
for(i=1;i<=n;++i){
int hh=end[i];
if(hh<0) add(i,tt,0,-hh);
else add(ss,i,0,hh),sum+=hh;
}
add(t,s,0,inf);//有源汇的别忘了连回去一下
int maxflow=0;
while(bfs(ss,tt))
maxflow+=dinic(ss,inf,tt);
if(maxflow==sum){
maxflow=0;//要为0哦
vis[ss]=1;vis[tt]=1;
while(bfs(s,t)) maxflow+=dinic(s,inf,t);
printf("%d\n",maxflow);
}
else printf("please go home to sleep\n");
return 0;
}
【有源汇有上下界的最小流】
(emmm……这个第8组数据没卡过去,求大佬帮优化Orz)
哈,过了,只要删掉dinic里面的一条语句 就过了
但思路总归没错,最小流在学习了最大流后就很好理解啦
……………………思考……………………
最大流在最后的时候从 s 到 t 跑一遍最大流,看还能增加多少流量
那最小流就倒着来呗,从 t 到 s 跑一遍最大流,看最多能退回去多少,退完过后不就是最小流的情况了吗?
还需要注意一点最小流最后跑dinic的时候还要删掉 t~s的这条边,为了防止退流时多退导致不合法
这个代码去掉源汇点就用的方法一
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<iostream>
#include<queue>
#define N 50010
#define M 405003
#define inf 0x7fffffff
#define in read()
using namespace std;
int n,m,ss,tt,s,t;
int nxt[M],head[N],to[M],cap[M],cnt=1,end[N];
int cur[M],lev[M];
void add(int x,int y,int z){
nxt[++cnt]=head[x];head[x]=cnt;to[cnt]=y;cap[cnt]=z;
nxt[++cnt]=head[y];head[y]=cnt;to[cnt]=x;cap[cnt]=0;
}
bool bfs(int hh,int ee){
memset(lev,-1,sizeof(lev));
memcpy(cur,head,sizeof(head));
queue<int > q;////////
q.push(hh);lev[hh]=0;
while(!q.empty()){
int u=q.front();q.pop();
for(int e=head[u];e;e=nxt[e]){
int v=to[e];
if(cap[e]>0&&lev[v]==-1){
lev[v]=lev[u]+1;
if(v==ee) return true;
q.push(v);
}
}
}
return false;
}
int dinic(int u,int flow,int ee){
if(u==ee) return flow;
int res=0;
for(int &e=cur[u];e;e=nxt[e]){
int v=to[e];
if(cap[e]>0&&lev[v]>lev[u]){
int delta=dinic(v,min(flow-res,cap[e]),ee);
if(delta){
cap[e]-=delta;cap[e^1]+=delta;
res+=delta;if(res==flow) break;
}
}
}
//这个不要哦这个是假优化,不要的话时间从1006ms降到46ms if(res<=flow) lev[u]=-1;
return res;
}
inline int read(){
int ans=0;
char ch=getchar();
while(!isdigit(ch))ch=getchar();
while(isdigit(ch))ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
return ans;
}
inline void write(int x){//输出优化
if(x>9)write(x/10);
putchar((x%10)^48);
}
int main(){
n=in;m=in;s=in;t=in;
int i,j,k;
ss=0;tt=n+1;
for(i=1;i<=m;++i){
int u,v,ll,rr;
u=in;v=in;ll=in;rr=in;
add(u,v,rr-ll);
end[v]+=ll;end[u]-=ll;
}
int sum=0,first;
add(t,s,inf);//有源汇的别忘了连回去一下
first=cnt-1;
for(i=1;i<=n;++i){
if(end[i]<0)
add(i,tt,-end[i]);
else if(end[i]>0)
add(ss,i,end[i]),sum+=end[i];
}
int maxflow=0;
while(bfs(ss,tt))
maxflow+=dinic(ss,inf,tt);
if(maxflow==sum){
maxflow=cap[first^1];
for(i=first;i<=cnt;++i) cap[i]=0;
while(bfs(t,s)) maxflow-=dinic(t,inf,s);
write(maxflow);
}
else printf("please go home to sleep\n");
return 0;
}
#define debug(x) cerr<<#x<<'='<<x<<endl
这个东西是用来防止傻瓜忘了关调试语句而搞出来的,用了这个宏之后这个输出的数只会显示在显示屏上,不会输出在文件里面(也就是不是影响答案)
相当于如果你写:printf (“%d", x )这个来输出 x 作为调试,你改为debug(x),假使忘了关也不会影响答案,那么如果要关的话也是更容易,直接在宏定义那里注解掉即可