1. 前言
本篇博文为 EK 算法求解最大流。
在往下看之前,请先确保您已经了解网络流的一些基础定义,包括但不限于网络,流量,源点,汇点,最大流定义。
如果您对上述定义有一部分不了解,可以前往这篇博文查看:
算法学习笔记:网络流#1——有关内容+算法导航
2. 例题
模板题:
P3376 【模板】网络最大流
P.S.:洛谷还有一道加强版最大流,那道题只能使用 HLPP(最高标号预留推进)加上各种卡常优化来写,但是那个不在讨论范围之内。
PP.S.:如果您想知道那道题到底卡常有多疯狂的话,到
那道题
的题解区里面看一下就知道了。
2.1 详解
假设现在来了一张网络和一只猴子。
(绘图工具:Windows 10 的 Microsoft Whiteboard)
现在这只猴子想要求解这张图的最大流。
于是这只猴子选了下面这一条红色路径,汇点
t
t
t
增加 1 流量:
然后这只猴子想要继续增加汇点流量(下称『推流』),选了下面这条路径。结果它发现不能推流了,于是它得出答案为 1,然后去吃水果了。
但是聪明的您一定能够发现,正确答案是 2,如下图所示:
猴子吃完水果后发现您得到了正确的最大流,很生气,于是它决定采用爆搜形式解决这道题。
解决完后它得意洋洋的走了,但是我们不满足于爆搜啊,肯定要找到一种更加优秀的写法。
先反观一下上述解法,您会发现猴子错误的原因:做事太快了,都不带思考。
没办法,程序不能思考,
但是我们可以让程序反悔!
什么意思呢?
比如说还是这张图,还是这条路径,但是我们动一点手脚:
发现什么了吗?
没错,这张图上出现了反向边。
反向边有什么用呢?
比如说还是这只猴子,在获得反向边加持之后,还想走下面这条路径。
于是它将
s
s
s
往
2
2
2
运输了 1 流量之后,发现正向边没得走了,但是存在一条反向边
(
2
,
1
)
(2,1)
(
2
,
1
)
。于是它就走了,然后得到了正确结果 2。
您可以这样理解反向边的作用:
比如现在有
I
N
F
INF
I
N
F
个人要从
s
s
s
到
t
t
t
,边的流量表示这条路上当前还有几张火车票。假设他们只能坐火车。
于是在 1 个人走了
s
−
>
1
−
>
2
−
>
t
s->1->2->t
s
−
>
1
−
>
2
−
>
t
之后,又有一个人想走
s
−
>
2
−
>
t
s->2->t
s
−
>
2
−
>
t
。
结果他发现走到
2
2
2
之后没票了!这怎么办啊,不行不行。
于是他通过
2
−
>
1
2->1
2
−
>
1
的反向边发现有一个人是从
1
1
1
来的,
而且这个人可以走
1
−
>
t
1->t
1
−
>
t
来到达
t
t
t
。
于是他与之前的人协商了一下,那个人退了票,然后走了
1
−
>
t
1->t
1
−
>
t
。
于是就有两个人可以到达
t
t
t
。
也就是说,反向边可以提供反悔功能。
但是注意到上面那句加粗的话了吗?
接下来给出
网络流
中『增广路』的定义:
-
增广路:如果存在一条路使得
ss
s
能够到达
tt
t
且
tt
t
的流量增加,那么这条路就叫做增广路。
于是上面的做法就是不断寻找增广路,直到找不到为止。
但是这为什么是正确的?
2.2 正确性证明
本证明同时证明了最大流=最小割。
证明如下:
考虑割掉一些边,使得源点与汇点不连通且点集个数为 2。
记
s
s
s
所在的点集为
S
S
S
,
t
t
t
所在的点集为
T
T
T
。
显然,进入
t
t
t
的总流量是由
T
T
T
内的点给的,而流出
s
s
s
的流量经过被割掉的边进入了
T
T
T
。
于是我们有这样一个式子:
-
原先
SS
S
到
TT
T
的流量 = 网络总流量
显然,无论怎样构造割,都有这个结果。
那么假设我们知道最大流为
F
l
o
w
Flow
F
l
o
w
:
-
在任意构造的割
SS
S
到
TT
T
中,必有:原先
SS
S
到
TT
T
的流量
≤F
l
o
w
\leq Flow
≤
F
l
o
w
。
那么现在,在经过上述过程之后,网络没有增广路了。
令当前的网络叫做『残量网络』,那么在残量网络中,令
S
S
S
表示
s
s
s
能够到达的所有点,
T
T
T
为
S
S
S
在
V
V
V
中的补集,
V
V
V
为点集。
那么,在残量网络中
S
S
S
向
T
T
T
流动的流量 = 原先这些被割断的边的容量。
根据上面所证明的,有:网络总流量 = 原先这些被割断的边的流量。
考虑到所有割流量
≤
F
l
o
w
\leq Flow
≤
F
l
o
w
,此时达到的不等式的交界处。
因此,此时得到的网络总流量必为最大流,同时证明了
最小割 = 最大流
,证毕。
2.3 做法与代码
实现就很关键了。
在寻找增广路的时候,有两种算法:DFS 与 BFS。
选择哪种算法,将会直接导致程序是 TLE 还是 AC。
为什么这么说呢?
先看看 DFS。
如果采用 DFS 来实现上述不断寻找增广路的过程,叫做 FF 算法。
大体思路就是不断增广,寻找增广路,没有了就结束算法。
很遗憾的是,FF 算法被卡掉了,几乎跟暴力没啥两样。
为什么?究其原因,就是因为
DFS 的效率太低了!
首先考虑每一次我们只能找出一条增广路,DFS 的复杂度至少
O
(
n
)
O(n)
O
(
n
)
,如果绕的路足够长或者有环,FF 就会彻底 TLE,其实就相当于是跟猴子一样不断暴力,
那么 BFS 呢?
在学搜索的时候各位应该都听过这么一句话:同等状态下,BFS 比 DFS 要优。
如果采用 BFS 写法,大体思路仍然是不断寻找增广路,但是因为 BFS 不会陷入环的陷阱,效率就高了很多。
而这个算法,就叫做 EK 算法。
如果思路理解了,代码应该也是好理解的吧qwq
关于存边与建反向边:
需要注意的是,边的编号要从 2 开始。
为什么呢?这样可以巧妙利用异或的性质:偶数异或 1 就是加一,奇数异或 1 就是减 1。
采取从 2 开始,那么第一组边的编号是 2,3,第二组边是 4,5。
于是在取反向边和改变边权的时候就很方便了。
还有,强烈推荐使用链式前向星,因为 vector 存图在网络流里面简直是太!难!写!了!
代码:
/*
========= Plozia =========
Author:Plozia
Problem:P3376 【模板】网络最大流——EK 写法
Date:2021/3/18
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::queue;
typedef long long LL;
const int MAXN = 200 + 10, MAXM = 5000 + 10;
LL INF = 0x7f7f7f7f7f7f7f7f;
int n, m, s, t, cnt_Edge = 1, Head[MAXN], pre[MAXN], vis[MAXN];
//pre:走过来的边的编号 vis:有没有访问过,防止重复走增广路
LL dis[MAXN], ans;//dis:当前点的流量
struct node
{
int to; LL val; int Next;
}Edge[MAXM << 1];//注意 * 2
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return (fh == 1) ? sum : -sum;
}
void add_Edge(int x, int y, int z) {Edge[++cnt_Edge] = (node){y, (LL)z, Head[x]}; Head[x] = cnt_Edge;}
LL Min(LL fir, LL sec) {return (fir < sec) ? fir : sec;}
bool bfs()
{
queue <int> q; q.push(s);
memset(vis, 0, sizeof(vis));//清空
vis[s] = 1; dis[s] = INF;//初始点流量为无穷大
while (!q.empty())
{
int x = q.front(); q.pop();
for (int i = Head[x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (Edge[i].val <= 0) continue;//只考虑大于 0 的边
if (vis[u] == 1) continue;//不重复走增广路
pre[u] = i; vis[u] = 1;//标记
dis[u] = Min(dis[x], Edge[i].val);//推流
q.push(u);
if (u == t) return 1;//找到增广路
}
}
return 0;//没有增广路了
}
void update()
{
for (int i = t; i != s;)
{
int v = pre[i];
Edge[v].val -= dis[t]; Edge[v ^ 1].val += dis[t];
i = Edge[v ^ 1].to;
}
ans += dis[t];//更新增广路上的边权
}
int main()
{
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; ++i)
{
int x = read(), y = read(), z = read();
add_Edge(x, y, z); add_Edge(y, x, 0);
}
while (bfs()) {update();}//不断找增广路
printf("%lld\n", ans);
return 0;
}
3. 总结
EK 求解最大流的核心思路就是利用 BFS 不断寻找增广路,来更新一路上的边权。
当然,EK 比较容易被卡,所以接下来将会介绍一种基于 FF 思路的更强的算法:dinic 算法(注意不是 EK)。