网络流1:最大流
网络流1:最大流
概念定义
流网络
流网络是一种特殊的有向图,由点集 \(V\)、边集 \(E\) 构成,其中点集包括源点和汇点,每条边有流量上界。
- \(E\) 中每条边 \((u,v)\) 存在一个叫“容量”的权值,记作 \(c(u,v)\),若 \((u,v) \notin E\),可以视作 \(c(u,v) = 0\)。
- \(V\) 中有两个特殊的点,即源点 \(s\) 和汇点 \(t\)。
流网络一般记为 \(G=(V,E)\)。
可行流
对于流网络 \(G\),若一个流满足以下两个性质,则称之为可行流,一般记作 \(f\)。
可行流需满足如下限制:
- 容量限制:若该可行流在任意边 \((u,v)\) 的流量 \(f(u,v)\) 满足 $0 \le f(u,v)\le c(u,v) $,则该流满足容量限制
- 流量守恒:若对于除 \(s\)、\(t\) 外任一点 \(u \in V\) 都满足 $\sum _{v \in V}f(v,u) = \sum _{v \in V} f(u,v) $,则该流满足流量守恒。
可行流的流量定义为 \(s\) 流出去的流量,或 \(t\) 流入的流量。
可行流不一定是最大流,甚至扩展一下流量可以是 \(0\)(即没有任何流)。
残余网络
就字面意思来理解,一个可行流会导致一些边的容量被占用。而这些被占用的容量就等于“消失”了。刨掉这些容量后,剩下的就是一张“残留网络”。
概念辨析:一张流网络对应一堆可行流,一个可行流与其流网络合在一块对应一个残余网络。与残余网络对应的是可行流。
如果可行流流量为 \(0\),残余网络即原图。这可以被视作“初始状态”。
严格定义:
对于流网络的 $G=(V,E) $ 和一个可行流 \(f\),我们定义一个残余网络 $ G_f=(V,E'),E'={(u,v)|c_f(u,v)>0} $,其中 $ E'$ 的容量定义为容量残余,即 $c'(u,v)=c(u,v)-f(u,v) $。
增广路
我们的目标是最大流。假设说,我们有一个可行流,一个朴素的想法是,能否通过一些操作,让可行流逼近最大流?想必是可以的!
接下来,我们探讨一下这个“操作”。首先,我们考虑拆分可行流的结构,不难想到,一条可行流可以由多条满足流量守恒和容量限制的路径拼接而成。其中每条路径所有边种的流量是相等的,对于每个点(边),其在可行流中的流量为每个路径中的流量相加。
不难证明这样操作每个点仍然满足流量守恒。由拼接的定义也不难证明容量限制。
顺带一提,其实,可以通过某种映射定义来定义同一张流网络中流的加减法,详见隔壁《网络流0:奇思妙想》。
可行流能由路径拼接而成,那么,不难想到,对于每个可行流,都可以通过拼接更多的这样的路径来增加其流量。而这样的路径肯定在残余网络上。
这样的路径,称之为“增广路”。
严格定义:
对于一张残余网络 \(G_f\),一条 \(s\) 到 \(t\) 的路径称之为“增广路”。对于一条增广路,其流量定义为路径经过的边的容量的最小值。将增广路与原路相加,使整个网络的流量增加,这一过程被称为增广。最大流的求解可以被视为若干次增广分别得到的流的叠加。
但是,增广路不是随便加的。比如说下图( \(1\) 和 \(4\) 分别是源汇点):
4 5
1 2 1
2 3 1
3 4 1
1 3 1
2 4 1
如果我们不幸增广了 1->2->3->4,那么我们将没有其他增广路,流量为 \(1\);但是,最大流显然为 1->3->4 和 1->2->4,流量为 \(2\)。
因此,我们需要新建反向边。我们约定 $f(u,v)=-f(v,u) $,这一性质可以通过在每次增广时引入退流操作来保证。
然后,我们就会发现一个很反人类的情况:有一些边容量是负数!但实际上,真正有意义的是剩余容量 \(c_f\)。而且,实际上,作者体感这个性质更多是为了让退流这个操作从正向的负数变成反向的正数。
拿上面的例子:1->3->4 加 1->2->4 与 1->2->3->4 加 1->3->2->4 实际是相等的。
实现上,如果使用链式前向星存储,则可以考虑从 \(0\) 开始作为下标,这样一对边在编号上是成对的,可以直接通过异或进行转化。
不难发现,只要有增广路,一定能使流更大。但是,如果没有增广路,是否一定不存在更大流呢?
有一个定理能够证明不存在这种情况。这个定理也证明了最大流等于最小割,详情见割的部分吧(俗称先咕着)
到此为止,我们已经学会了所有基本定义,接下来,让我们上手实现吧!
EK 算法
由上面知,我们只需要找增广路就可以啦。
每次 \(O(m)\) bfs,暴力找最短路即可。
增广上界可以被证明为 $O(nm) $ 的,但是我不会证,如果想要深度学习可以看 OI wiki,我就懒一下了 awa
代码略我咋这么懒
dinic
体感上这是最常见的网络流算法了
注意到,我们一次增广,其实可以增广一个可行流而非增广路。那么,有没有什么方式可以一次增广多个增广路呢?
注意到,网络流没法直接贪心的原因是因为这不是一个 DAG,那我们把他变成一个 DAG 不就可以了。我们在增广之前先跑一个 bfs 分层,强制上层点只能往下层点走,强行变成一个 DAG。
bool bfs()
{
queue <int> q;
memset(flo,0,sizeof(flo));
q.push(S);flo[S]=1;
while(!q.empty())
{
int x=q.front();q.pop();
for(int i=hd[x];~i;i=e[i].nx)
{
int v=e[i].v,c=e[i].c;
if(!c||flo[v]) continue;
if(v==T) return 1;
q.push(v);
}
}
return 0;
}
然后,我们再一次进行 dfs,不过这次找到一条路后不会停止,而是会回溯到前面找到其他路。合在一起后,就会形成一个增广流。
int dfs(int x,int limit)
{
if(x==T) return limit;
int flow=0;
for(int i=hd[x];~i&&flow<limit;i=e[i].nx)
{
int v=e[i].v,&c=e[i].c,&cc=e[i^1].c;
if(!c||flo[v]!=flo[x]+1) continue;
int num=dfs(v,min(limit-flow,c));
flow+=num;
c-=num;cc+=num;
}
return flow;
}
我们每次增广都要进行 bfs,但是 bfs 如果无法抵达 \(T\),那说明找不到增广路了,算法结束。
int dinic(int s,int t)
{
S=s,T=t;
int res=0;
while(bfs())
{
int flow=0;
while((flow=dfs(S,INF))) res+=flow;
}
return res;
}
到此为止,白板 dinic 就做完了。但是,dinic 是一个对优化很敏感的算法,接下来我们介绍一下两个常见优化。
当前弧优化:注意到,我们遍历边的顺序是固定的。如果在某次操作后,一条边结束了并且来到了下一条边,那说明这条边已经到了极限,我们再下次来到这个点时可以跳过这个边。也就是说,我们只需要维护一个全局指针,表示当前点第一个没有增广到极限的边。这个指针一般称之为 \(cur\)。
爆点优化我也不知道叫什么但这个名字挺形象的:如果我们来到了一个点,发现这个点对当前流没有产生任何可行流,那么这个点就被增广到极限了。此时,我们给这个点打标记,表明这个点达到了极限,下次路过时无需访问。
其实,这两个优化本质上是对增广到达极限的边/点进行跳过。
实现如下:
namespace Dinic{
#define Tpl LL
struct E{
int v,nx;Tpl c;
}e[M*2];
int hd[N],ecnt=-1;
void add(int u,int v,Tpl c){
e[++ecnt]={v,hd[u],c};hd[u]=ecnt;
e[++ecnt]={u,hd[v],0};hd[v]=ecnt;
}
void init(){
memset(hd,-1,sizeof(hd));
ecnt=-1;
}
int S,T;
int cur[N],flo[N];
bool bfs(){
queue <int> q;
memset(flo,0,sizeof(flo));
q.push(S);flo[S]=1;
while(!q.empty()){
int x=q.front();q.pop();
cur[x]=hd[x];
for(int i=hd[x];~i;i=e[i].nx){
int v=e[i].v;Tpl c=e[i].c;
if(!c||flo[v]) continue;
flo[v]=flo[x]+1;
cur[v]=hd[v];//这句加不加好像无所谓,但是我在做一些题时有微小提升
if(v==T) return 1;
q.push(v);
}
}
return 0;
}
Tpl dfs(int x,Tpl limit){
if(x==T) return limit;
Tpl flow=0;
for(int i=cur[x];~i&&flow<limit;i=e[i].nx){//faster of cur
cur[x]=i;//faster of cur
int v=e[i].v;Tpl &c=e[i].c,&cc=e[i^1].c;
if(!c||flo[v]!=flo[x]+1) continue;
Tpl num=dfs(v,min(limit-flow,c));
if(!num) flo[v]=0;//faster of Node Boom
flow+=num;
c-=num;cc+=num;
}
return flow;
}
Tpl dinic(int s,int t){
S=s,T=t;
Tpl res=0;
while(bfs()){
Tpl flow=0;
while((flow=dfs(S,INF))) res+=flow;
}
return res;
}
int jiliguala=(init(),1);//小技巧,如果老是忘 init,就直接把开局的 init 刻模板里。
#undef Tpl
}using Dinic::init;using Dinic::add;using Dinic::dinic;
离谱错误大赏警示后人
if(!c||flo[v]!=flo[x]+1) continue;中忘记检查!c,慢了很多;- \(flo\) 的关系写反;
- 忘记增光后处理残留网络;
- 忘记添加
if(!num) flo[v]=0;; - 《
&c=e[i].c,&cc=e[i].c;》; - \(T\) 的值没算好;这也启示我们,拆点时一定要算好点数,不要有两个点重了;
~i写成i死循环……
参考资料
update on 7/14
有反应说过不去模板题,因为模板题要开 long long……;优化模板,现在支持多 Template 了。

浙公网安备 33010602011771号