【学习笔记】网络流
前言:
网络流就像自来水厂到你家的水管,自来水厂(源点S)源源不断的提供水,水通过不同水管汇集于你家(设自来水厂的水全到你家,汇点T)。自来水厂到你家的水管网是一个复杂的有向图,每一节水管都有一个最大承载流量(容量)。自来水厂不放水,你家就没水了。但是就算自来水厂拼命地往管网里面注水,你家收到的水流量也是上限,毕竟每根水管承载量有限。这就是一个有向图,所有的水都从一个点流出(水厂),最后全部汇聚到一个点(你家)。
网络
网络(流网络)是指一个有向联通图,由一个点集和一个边集构成 G=(V,E)。
- 图中每个边都有一个属性,称为容量 c(u,v),表示最大能够通过的水量(或其他限制条件)。
- 图中有两个特殊点:源点(S)和汇点(T)。源点 S 有无限多的水流可以向外流出,汇点 T 可以接受无限多的水流。
ps:不考虑反向边,如果有反向边,通过加点变成没有反向边的图。
如果一条边不存在,则定义这条边的容量是0。
流
对于一个网络流图 G=(V,E),每条边(u,v)上给定一个实数 f(u,v),f(u,v) 为 边(u,v) 上的流量。
对于任何一条有向边(u,v),f(u,v)=-f(v,u)
一个可行流f需满足两个条件:
-
流量守恒:
除源点和汇点外,流入其余每一个点的流量和等于流出这个点的流量和,不存储流量。
-
容量限制 0
f(u,v) c(u,v)
我们永远赚不到自己认知以外的钱,水管也永远装不下比自己容量还大的水
|f| 表示可行流的流量值,定义为从源点流出的流量或汇点流入的流量.
|f| =
和流网络的关系:一个流网络中有很多个可行流,G
残留网络
流网络里的每一个可行流f都有自己的残留网络
残留网络同样也是由一个点集和一个边集构成,
- 点集包含原网络所有点(
=V), - 边集包括原网络中所有边和所有反向边(
=E E中所有反向边)。
而残留网络中,边的容量c'(u,v)是原网络的残留容量
c'(u,v)=
流量计算:|f+f'|=|f|+|f'|
流量相加指的是每条边对应相加:残留网络和原网络边的方向相同,累加;相反,相当于退回的流量,减去;
关于反向边
反向边是一个很牛逼的反悔机制,可以把前面流的流量退回去。这样,就能多去考虑被之前的通路阻断的情况,不会漏解。换句话说,反向边的存在让我们可以在所有的情况里选取最符合我们所需的情况。
原网络的可行流(f)和残留网络可行流(f’)有啥关系?
f'+f也是原网络G的另外一个可行流。
证明:
1.流量守恒证明
2.容量限制证明
- 考虑正向边,当c'(u,v)=c(u,v)-f(u,v) 时
- 考虑反向边,当c'(u,v)=f(v,u)时
增广路
残留网络里,从源点沿着流量 > 0的边能够走到汇点,这样的路径为增广路径。
对于当前流网络里的可行流来说,残留网络里无增广路径,则该可行流为最大流。
最大流
对于一个流网络来说,有很多可行流最大流是最大可行流。
EK算法
基于FF方法的一种算法
由最大流的定义得知,当残留网络里,不存在增广路时,当前可行流为最大流,那么EK算法就是把增广路一条一条的找到,那么不断地消除这一条条增广路,从而求得最大流。对增广路对应的原图里面的路径增流,就可以消除这条增广路。
- 找增广路
在残量网络中,任意找一条从 S 到 T 的路径,边权均不为 0,则这条路径是一条增广路。这里我用的是bfs - 更新残留网络
在找到一条增广路后,找出路径上的边权最小值 x ,然后把路径上所有边权都减去x(也就是找增广路后,这条路径上流过 x 的流量)
我们不断的重复上面两个操作,直到找不到增广路为止,则没有一条路径可以给汇点增加流量。此时,汇点汇集的流量为最大流。
那么如果一条更优的路径被前面消除的一条路截断了怎么办呢?
这个时候,反向边的反悔机制,就起到了很牛逼的作用,我直接反悔,把这个之前通过增流消除的增广路进行退流,把之前增加的流量退回来,对当前更优的增广路进行消除,就ok了
代码实现:
点击查看代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1010,M=20010,inf=1e8;
int e[M],ne[M],f[M];
int q[N],d[N],h[N],pre[N],idx;
int m,n,S,T;
bool st[N];
void add(int a,int b,int c)
{
e[idx]=b,f[idx]=c,ne[idx]=h[a],h[a]=idx++;
e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;
}
bool bfs()
{
int tt=0,hh=0;
memset(st,0,sizeof st);
q[0]=S,st[S]=true,d[S]=inf;
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];~i;i=ne[i])
{
int ver=e[i];
if(!st[ver]&&f[i])
{
st[ver]=true;
d[ver]=min(f[i],d[t]);
pre[ver]=i;
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
inline int EK()
{
int ans=0;
while(bfs())
{
ans+=d[T];
for(int i=T;i!=S;i=e[pre[i]^1])
{
f[pre[i]]-=d[T],f[pre[i]^1]+=d[T];
}
}
return ans;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&S,&T);
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
printf("%d\n",EK());
return 0;
}
Dinic算法
blablablabla...
对EK算法的一种优化
在EK的基础上建立分层图,
每次一层层跑最短路 相比于EK算法 减少了很多不必要的循环 可以更高效的消除增广路
点击查看代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=10100,M=200100,inf=1e8;
int e[M],ne[M],f[M];
int q[N],d[N],h[N],cur[N],idx;
int m,n,S,T;
void add(int a,int b,int c)
{
e[idx]=b,f[idx]=c,ne[idx]=h[a],h[a]=idx++;
e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;
}
bool bfs()
{
int tt=0,hh=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=h[S];
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];~i;i=ne[i])
{
int ver=e[i];
if(d[ver]==-1&&f[i])
{
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
inline int find(int u,int limits)
{
if(u==T) return limits;
int flow=0;
for(int i=cur[u];~i&&flow<limits;i=ne[i])
{
int ver=e[i];
cur[u]=i;
if(d[ver]==d[u]+1&&f[i])
{
int t=find(ver,min(f[i],limits-flow));
if(!t) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
inline int Dinic()
{
int ans=0,flow;
while(bfs())
{
while(flow=find(S,inf)) ans+=flow;
}
return ans;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&S,&T);
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
printf("%d\n",Dinic());
return 0;
}
无源汇上下界可行流
点击查看代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1010,M=30010,inf=1e8;
int e[M],ne[M],f[M],l[M];
int q[N],d[N],h[N],cur[N],idx,A[N];
int m,n,S,T;
void add(int a,int b,int c,int d)
{
e[idx]=b,f[idx]=d-c,l[idx]=c,ne[idx]=h[a],h[a]=idx++;
e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;
}
bool bfs()
{
int tt=0,hh=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=h[S];
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];~i;i=ne[i])
{
int ver=e[i];
if(d[ver]==-1&&f[i])
{
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
inline int find(int u,int limit)
{
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i&&flow<limit;i=ne[i])
{
int ver=e[i];
cur[u]=i;
if(d[ver]==d[u]+1&&f[i])
{
int t=find(ver,min(f[i],limit-flow));
if(!t) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
inline int Dinic()
{
int ans=0,flow;
while(bfs())
{
while(flow=find(S,inf)) ans+=flow;
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
S=0,T=n+1;
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
add(a,b,c,d);
A[a]-=c,A[b]+=c;
}
int tot=0;
for(int i=1;i<=n;i++)
{
if(A[i]>0)
{
add(S,i,0,A[i]);
tot+=A[i];
}
else if(A[i]<0)
{
add(i,T,0,-A[i]);
}
}
if(Dinic()!=tot) puts("NO");
else
{
puts("YES");
for(int i=0;i<2*m;i+=2)
{
printf("%d\n",f[i^1]+l[i]);
}
}
return 0;
}
有时间再写..
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端