此博客为我在高二时学习网络流写下的文章,最初发在洛谷上,反响不错,作为第一篇文章发在CSDN上试水。
何为网络流?
给你一个有向图表示水塔到你家的各种水管通流方向,边表示水管一秒内最大能通多少升的水,水管可能交叉相接又分叉,求从水塔到你家一秒内最大能流多少升水。
也就是网络图上的通流问题。
注意,这里说的水塔就是源,水龙头就是汇。
好比这张图的最大流量就是 19 19 19 (想一想,为什么。
很容易发现,如果我们是程序(自己想象一下即可,不需要动手实践,开玩笑的),那么求解最大流量在我眼中就好像是个贪心,只要是能流的管道我直接流就行了,无非是在dfs
过程中求一个路径最小值,在路径上所有边上减去这个最小值,一直重复到无管可流就行了,我们把能流的一条路称作增广路。
就会流成这样,这张图也就是残量图,就是当前流剩的图。
对于上面的例子这个思路是显然正确的。
但是如果在水管之间横着插上一个细管,这个做法就可能变成翔。
只要你点了大眼观察技能,你就能发现显然上图最大流应该是2,可是你无敌的算法跑出来是1,这是怎么一回事呢?是因为你dfs了错误的顺序,根据生活常识你不能向水龙头里吹把水吹回去让水流反悔,那么你可能需要一个全新的操作,建立反向边,来达到把水吹回去的目的,没错,是真的吹回去了。
还是这个把厕所下水道水管接到厨房饮用水管的图(并不。但是我们建立了反向边
每次流完一条路,正向边流量减了多少,就把对应的反向边流量加多少。
然后把流反向把边当成一种合理的决策,继续dfs。
得出现在的答案是2。
这个做法是正确的。
因为(1,2,3,4)流过之后,(1,3)再流过来,发现(1,2)流着(3,4)这条道路,于是大喊:“明明是我该流的,你为什么这么熟练啊。”嗯,一看就不是什么正经流。
然后(1,3)把水流沿着(3,2)推回去,把(3,2)这条边复原。
此时(1,3,2)再流过(2,4),告诉他:“你看你这他妈的不是能流?”,然后(3,4)的流量给了(1,3),(1,2)乖乖地流(2,,4)。
然后最后的结果是(1,3)流(3,4),(3,2)退流,(1,2)流(2,4),
咱们就当无事发生过,继续dfs
求增广路。
总而言之,反向边就相当于给了你程序反悔的机会。
那么如何实现反向边?已知build
建边函数用到的计数变量cnt
都是连续的,可得反向边的编号就是正向边的编号加一,不妨设cnt
初始值是
1
1
1 ,那么以第一组正反边为例,正向边编号是
2
2
2 ,反向边是
3
3
3 ,都可以异或
1
1
1 得到彼此。
至此,总结出思路,从源开始,只要是边上残量不为 0 0 0 ,我们就流过这条边,然后递归到找到终点为止,顺带在边上减去实际流量。
int dfs(int now,int rem)//rem是残量,也就是从源过来的【水流量】
{
if(now==n)return rem;
int tmp=rem;
for(int i=head[now];i;i=e[i].next)
{
int v=e[i].to;
if(e[i].w)
{
int k=min(e[i].w,tmp);//路径上最大流量和边残量中取个较小值
int dlt=dfs(v,k);//解出通过这条边可以流多少流量
e[i].w-=dlt;e[i^1].w+=dlt;//优化:反向边
tmp-=dlt;//分流了 流量减少
if(!tmp)break;
}
}
return rem-tmp;//返回实际流了多少流量
}
基础思路没问题了,让我们用一个极端的例子看一看目前得出的算法低效之处:
对于这张图,如果你的程序采取了(1,2,3,4)流法,那么反向边(3,2)就会变成 1 1 1 ,然后若你的程序再采取(1,3,2,4)的流法,(2,3)复原,此时流量为 2 2 2 ,如此往复,程序就会在执行 1998 1998 1998 次之后成功得出答案。但是明显我们只需要流 2 2 2 次就能得出答案。
这时候我们就需要分层图的思想,借用让重力让“水流只向低处流”的思想。
核心思想就是用bfs处理一遍图,处理出图上每个点的深度后再dfs
。
然后dfs每次递归都只能向更深的地方递归,就像水流只能从高山上向低洼的城市中心流动,而不是在“环城路”上重复流动浪费资源。
大概就是这样写,嗯。
bool bfs()//求增广路
{
memset(dis,0,sizeof(dis));
dis[1]=1;
queue<int>q;
q.push(1);//放入起点
while(!q.empty())
{
int u=q.front();q.pop();
rad[u]=head[u];//复原当前弧
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dis[v]&&e[i].w)//如果这个点还没有被遍历过,且允许通流(增广路的必要条件)
dis[v]=dis[u]+1,q.push(v);
}
}
return dis[n];//能流到终点欤否?
}
int dfs(int now,int rem)//rem是残量,也就是从源过来的【水流量】
{
if(now==n)return rem;
int tmp=rem;
for(int i=head[now];i;i=e[i].next)
{
int v=e[i].to;
if(dis[v]==dis[now]+1&&e[i].w)//优化:分层图
{
int k=min(e[i].w,tmp);//路径上最大流量和边容量中取个较小值
int dlt=dfs(v,k);//解出通过这条边可以流走多少流量
e[i].w-=dlt;e[i^1].w+=dlt;//优化:反向边
tmp-=dlt;//分流了 流量减少
if(!tmp)break;
}
}
return rem-tmp;//返回实际流了多少流量
}
int dicnic()
{
int ans=0;
while(bfs())ans+=dfs(1,1e9); //把起始的残量设为无穷大
return ans;
}
当然,这样优化后的dinic
还是不够优秀的。我们需要一个新的优化,当前弧优化。
名字听起来很牛逼,实际上就是一个去除冗余。
比如你看这张图,当你(1,3,4,…,9)求解完之后,实际上可能(4,6),(4, 7)等边已经不存在增广的可能了,也就是一滴都没有了,那你(1,2,4)再增广过来的时候,很多条路径其实根本不需要遍历,那么我们干脆设置一个rad
数组,记录下上一次增广到哪条边了,下次直接从rad
这条边开始遍历。
bool bfs()//求增广路
{
memset(dis,0,sizeof(dis));
dis[1]=1;
queue<int>q;
q.push(1);
while(!q.empty())
{
int u=q.front();q.pop();
rad[u]=head[u];//复原当前弧
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dis[v]&&e[i].w)//如果这个点还没有被遍历过,且允许通流(增广路的必要条件)
dis[v]=dis[u]+1,q.push(v);
}
}
return dis[n];//能流到终点欤否?
}
int dfs(int now,int rem)//rem是残量,也就是从源过来的【水流量】
{
if(now==n)return rem;
int tmp=rem;
for(int i=rad[now];i;i=e[i].next)//优化:当前弧
{
int v=e[i].to;rad[now]=i;
if(dis[v]==dis[now]+1&&e[i].w)//优化:分层图
{
int k=min(e[i].w,tmp);//路径上最大流量和边容量中取个较小值
int dlt=dfs(v,k);//解出通过这条边可以流走多少流量
e[i].w-=dlt;e[i^1].w+=dlt;//优化:反向边
tmp-=dlt;//分流了 流量减少
if(!tmp)break;
}
}
return rem-tmp;//返回实际流了多少流量
}
int dicnic()
{
int ans=0;
while(bfs())ans+=dfs(1,1e9);
return ans;
}
完整代码,以P1343为例,放上标程:
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=205,maxm=2005;
int n,m,x,cnt=1;
int head[maxn],rad[maxn],dis[maxn];
struct node
{int next,to,w;}e[maxm*2];
void build(int u,int v,int w)
{e[++cnt].to=v;e[cnt].w=w;e[cnt].next=head[u];head[u]=cnt;}
inline int read()
{
int x=0;char r=getchar();
while(r<'0'||r>'9')r=getchar();
while(r>='0'&&r<='9')
{x=x*10+r-'0';r=getchar();}
return x;
}
void init()
{
n=read();m=read();x=read();
for(int i=1,x,y,z;i<=m;i++)
{
x=read();y=read();z=read();
build(x,y,z);
build(y,x,0);//反向边
}
}
bool bfs()//求增广路
{
memset(dis,0,sizeof(dis));
dis[1]=1;
queue<int>q;
q.push(1);
while(!q.empty())
{
int u=q.front();q.pop();
rad[u]=head[u];//复原当前弧
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dis[v]&&e[i].w)//如果这个点还没有被遍历过,且允许通流(增广路的必要条件)
dis[v]=dis[u]+1,q.push(v);
}
}
return dis[n];//能流到终点欤否?
}
int dfs(int now,int rem)//rem是残量,也就是从源过来的【水流量】
{
if(now==n)return rem;
int tmp=rem;
for(int i=rad[now];i;i=e[i].next)//优化:当前弧
{
int v=e[i].to;rad[now]=i;
if(dis[v]==dis[now]+1&&e[i].w)//优化:分层图
{
int k=min(e[i].w,tmp);//路径上最大流量和边容量中取个较小值
int dlt=dfs(v,k);//解出通过这条边可以流走多少流量
e[i].w-=dlt;e[i^1].w+=dlt;//优化:反向边
tmp-=dlt;//分流了 流量减少
if(!tmp)break;
}
}
return rem-tmp;//返回实际流了多少流量
}
int dicnic()
{
int ans=0;
while(bfs())ans+=dfs(1,1e9);
return ans;
}
int main()
{
init();//读入数据
int a=dicnic();
if(a!=0)
{
if(x%a)printf("%d %d",a,(x-x%a)/a+1);
else printf("%d %d",a,x/a);
}
else printf("Orz Ni Jinan Saint Cow!");
return 0;
}