网络流!
前置知识
有向图 \(G=(V,E)\) 就叫做一个网络,每条有向边有一个属性 \(C(x,y)\) 叫做边 \((x,y)\) 的容量,即这条边最多所能容纳的流量,一般来说在一个网络中还有两个特殊的点,源点 \(s\) 和 汇点 \(t\)。
网络流中有一个非常重要的函数 \(f(x,y)\),它表示流经边 \((x,y)\) 的流量数值,因此它叫做网络的流函数。
流函数三大性质
这三个条件其实也是流函数的三大性质:
-
容量限制:每条边的流量总不可能大于该边的容量的。
-
斜对称:正向边的流量=反向边的流量。(\(f(x,y)=-f(y,x)\))
-
流量守恒:正向的所有流量和=反向的所有流量和。(就是总量始终不变)
残量网络:
在任意时刻,网络中所有节点以及剩余容量大于 \(0\) 的边构成的子图被称为残量网络。
最大流
对于上面的网络,合法的流函数有很多,其中使得整个网络流量之和最大的流函数称为网络的最大流,此时的流量和被称为网络的最大流量。
最大流能解决许多实际问题,比如:一条完整运输道路(含多条管道)的一次最大运输流量,还有二分图。
下面就来介绍计算最大流的两种算法:
EK 增广路算法和 Dinic 算法。
EK
增广路:
若一条从 \(s\) 到 \(t\) 的路径上所有边的剩余容量都大于 \(0\),则称这样的路径为一条增广路。(剩余流量:\(c(x,y)−f(x,y)\))
不断用BFS寻找增广路并不断更新最大流量值,直到网络上不存在增广路为止。
在 BFS 寻找一条增广路时,我们只需要考虑剩余流量不为 \(0\) 的边,然后找到一条从 \(s\) 到
\(s\) 的路径,同时计算出路径上各边剩余容量值的最小值 dis,则网络的最大流量就可以增加 dis。(经过的正向边容量值全部减去 dis,反向边全部加上 dis)
反向边:网络流中的一个重点。
为什么要建反向边?
因为可能一条边可以被包含于多条增广路径,所以为了寻找所有的增广路经我们就要让这一条边有多次被选择的机会
而构建反向边则是这样一个机会,相当于给程序一个反悔的机会。为什么是反悔?
因为我们在找到一个 dis 后,就会对每条边的容量进行减法操作,而直接更改值就会影响到之后寻找另外的增广路。
最劣时间复杂度为 \(O(nm^2)\)!
Dinic
EK 算法每次都可能会遍历整个残量网络,但只找出一条增广路。
那么能否一次找多条增广路呢?
Dinic 算法就可以实现:
与 EK 一样,我们仍要通过bfs来判断图中是否还存在增广路,但是 dinic 算法里的 bfs 略有不同,这次,我们不用记录路径,而是给每一个点分层,对于任意点 \(i\),从 \(s\) 到 \(i\) 每多走过一个点,就让层数多 \(1\)。
分层有什么用?
有了每个点的层数编号,对任意点 \(u\) 到点 \(d\) 的路径如果有 \(d[d]==d[u]+1\) ,我们就可以判断该路径在一条最短增广路上。
分完了层就要开始找增广路了。
DFS 登场!
优化:
其实只要加一点小小的改变,我们就可以在 dfs 时直接实现多路增广!
若能到达t则可以再次 dfs ,否则重新 bfs。(看起来没什么用)
再定义一个变量 res,用来表示这个点的流量用了多少。
然后在 dfs 时我们可以在找到一条增广路时不直接返回,而是改变 res 的值,如果 res 还没达到该点流量上限,可以继续找别的增广路。
优化2.0:传说中的当前弧优化!
我们定义一个数组 now 记录当前边(弧)(功能类比邻接表中的 head 数组,只是会随着 dfs 的进行而修改),
每次我们找过某条边(弧)时,修改 now 数组,改成该边(弧)的编号,
那么下次到达该点时,会直接从 now 对应的边开始(也就是说从 head 到 now 中间的那一些边(弧)我们就不走了)。
有点抽象啊,感觉并不能加快,然而实际上确实快了很多。
原因:
首先,我们在按顺序 dfs 时,先被遍历到的边肯定是已经增广过了(或者已经确定无法继续增广了),那么这条边就可以视为“废边”。
那么下次我们再到达该节点时,就可以直接无视掉所有废边,只走还有用的边,也就是说,每次 dfs 结束后,下次 dfs 可以更省时间。
最劣时间复杂度为 \(O(n^2m)\),薄纱 EK!
\(\LARGE \text{抽象.jpg}\)
#include<bits/stdc++.h>
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=a,_i=b;i<=_i;i=-~i)
#define bd(i,a,b) for(int i=a,_i=b;i>=_i;i=~-i)
using namespace std;
const int N=5e5+509,M=3e3+509,mod=998244353;
int tot=1,head[N],n,s,t,m;
int dis[N],pre[N],vis[N],ans;
int now[N];
int f[M][M];
inline int read(){int x;scanf("%lld",&x);return x;}
inline void write(int x,int F=1)
{
if(F==0) printf("%lld",x);
else if(F==1) printf("%lld\n",x);
else printf("%lld ",x);
}
struct E
{
int to,nxt,v;
}e[N<<1];
inline void add(int x,int y,int z)
{
e[++tot]=(E){y,head[x],z},head[x]=tot;
e[++tot]=(E){x,head[y],0},head[y]=tot;
}
bool bfs()
{
/*
//EKのBFS
fd(i,1,n) vis[i]=0;
queue<int> q;
q.push(s);
vis[s]=1;
dis[s]=1e18;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
if(!e[i].v) continue;//我们只关心剩余流量>0的边
int y=e[i].to;
if(vis[y]) continue;//这一条增广路没有访问过
dis[y]=min(dis[x],e[i].v);
pre[y]=i,vis[y]=1;//记录前驱,方便修改边权
q.push(y);
if(y==t) return 1;//找到了一条增广路
}
}
return 0;
*/
//dinicのBFS
fd(i,1,n) dis[i]=1e18;
queue<int> q;
q.push(s);
now[s]=head[s];
dis[s]=0;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
if(!e[i].v) continue;
int y=e[i].to;
if(dis[y]!=1e18) continue;
now[y]=head[y];
dis[y]=dis[x]+1;//给增广路上的点分层
q.push(y);
if(y==t) return 1;
}
}
return 0;
}
inline void EK()//更新所经过边的正向边权以及反向边权
{
int x=t;
while(x!=s)
{
int y=pre[x];
e[y].v-=dis[t];
e[y^1].v+=dis[t];
x=e[y^1].to;
}
ans+=dis[t];//累加每一条增广路经的最小流量值
}
inline int dinic(int x,int sum)
{
if(x==t) return sum;
int k,res=0;
for(int i=now[x];i&&sum/*该点流量没满,可以找*/;i=e[i].nxt)
{
now[x]=i;//当前弧优化!
int y=e[i].to;
if((!e[i].v)||dis[y]!=dis[x]+1) continue;
k=dinic(y,min(sum,e[i].v));
if(k==0) dis[y]=1e18;
e[i].v-=k,e[i^1].v+=k;
res+=k,sum-=k;//该点使用的流量增加
}
return res;//返回该点已使用流量
}
signed main()
{
#define FJ
#ifndef FJ
freopen("flower.in","r",stdin);
freopen("flower.out","w",stdout);
#endif
n=read(),m=read(),s=read(),t=read();
fd(i,1,m)
{
int u=read(),v=read(),w=read();
//注意重边!
if(f[u][v]==0)
{
add(u,v,w);
f[u][v]=tot-1;
}
else e[f[u][v]].v+=w;
}
while(bfs()) ans+=dinic(s,1e18);//直到网络中不存在增广路
// EK's:while(bfs()) EK(s,0);
write(ans);
return 0;
}
本文来自博客园,作者:whrwlx,转载请注明原文链接:https://www.cnblogs.com/whrwlx/p/18302046