本文搬运自洛谷大佬钱逸凡的博客
究级的最大流算法:ISAP与HLPP
最大流的定义及基础解法这里就不再赘述了
有需要的可以看之前写的劣质文章EK与Dinic求解最大流的方法了
前言
最大流算法目前有增广路算法和预流推进算法两种,增广路算法的思想是不断地寻找增广路来增大最大流,而预流推进算法的思想是……后面再讲。
先从最熟悉的增广路算法开始讲。
ISAP(Improved Shortest Augumenting Path):
其实Dinic已经足够高效了,但那些(闲着没事干)的计算机科学家们对Dinic的效率仍不满足,于是就有了ISAP算法。
在Dinic中我们每次增广前都进行了一次bfs来初始化每个点的深度,虽然一次标号增广了很多条路,但是我们还是很有可能要跑很多遍bfs导致效率不高。
那有没有什么办法只跑一次bfs呢?
那就是ISAP算法了!
ISAP运行过程:
1.从t到s跑一遍bfs,标记深度(为什么是从t到s呢?后面会讲)
2.从s到t跑dfs,和Dinic类似,只是当一个点跑完后,如果从上一个点传过来的flow比该点的used大(对于该点当前的深度来说,该点在该点以后的路上已经废了),则把它的深度加1,如果出现断层(某个深度没有点),结束算法
3.如果操作2没有结束算法,重复操作2
好抽象啊。
什么原理呢?
ISAP其实与Dinic差不多,但是它只跑一遍bfs,但是每个点的层数随着dfs的进行而不断提高(这样就不用反复跑bfs重新分层了),当s的深度大于n时(这就是为什么bfs要从t到s),结束算法。
先给一些数组定义:
1 | int dep[13000],gap[13000]; |
bfs部分:
1 | void bfs(){ |
可以看出ISAP里的bfs对 边权!=0 这一条件并没有限制,只要在后面的dfs里判断边权就好了。
接下来是dfs:
1 | int dfs(int u,int flow){ |
感觉和Dinic差不多。
只是多几句话来统计深度对应的点数。
这里要解释一下为什么要统计每个深度的节点数:
为了优化!
统计每个深度对应点数只为了这句话:
1 | if(gap[dep[u]]==0)dep[s]=n+1; |
因为我们是按照深度来往前走的,路径上的点的深度一定是连续的,而t的深度为0,如果某个深度的点不存在,那么我们就无法到达t了
此时直接结束算法可以大大节省时间。
接下来是主函数:
1 | int ISAP(){ |
由于dfs部分(有点)相当抽象,可以结合图示理解。
以下图为例:(反向边没有画,但是有反向边)

先 bfs 分层:(蓝色字体表示层数)

按照深度,我们只能走S->1->5->6->t这条增广路,流量为3
在dfs过程中,从5传到6时$flow==4$而在6号点的$used==3$,所以此时不会直接返回(见上方的代码),而是把6号点的深度加1,同理s,1,5的也要加1,也就是说,跑了这条增广路后,图变成了:

看见了吗?s->1->5->6->t这条路被封了,但是s->1->2->3->4->t这条路却能走了
按照深度,这次肯定是走s->1->2->3->4->t这条路。
这次的情况和上次不同,由于从2传到3时的flow==3而之后的路径的used都是3,所以2,3,4号点的深度不会改变,而1,s的情况与上次的那些点相同,深度会改变。
跑了这条路之后,图变成了:

图中出现了断层(深度为4的点没有了),所以一定不存在增广路了,可以直接结束算法了。
刚才那张图如果用Dinic会怎么样?
三遍bfs,两遍dfs!
与之相比,ISAP真是太高效了!
这里顺便说一下为什么终止条件是dep[s]<n
从上面的过程可以看出:每走一遍增广路,s的层数就会加1,如果一直没有出现断层,最多跑n-dep(刚bfs完时s的深度)条增广路(因为一共就n个点)
完整代码:
1 |
|
当然,这东西也可以当前弧优化(原理见 Dinic )
只需要改一点即可:
1 | while(dep[s]<n){ |
dfs部分的修改就不放代码了,应该都会。
接下来就是相当抽象的预流推进算法了
- 在讲解算法之前,我们先来回顾一下最大流是干嘛的:
- 水流从一个源点s通过很多路径,经过很多点,到达汇点t,问你最多能有多少水能够到达t点。
- 在刚面对这个问题时,你或许会有这样的思路:
- 先从s往相邻点拼命灌水,然后让水不停地向t流,能流多少是多少。
这就是预流推进:
能推流,就推流,最终t有多少水,最大流就是多少。
预流推进算法的思想:
在讲思想之前,先引入一个概念:
余流:
- 听名字就知道是什么意思了,即每个点当前有多少水。
预留推进算法的思想是:
- 1.先假装s有无限多的水(余流),从s向周围点推流(把该点的余流推给周围点,注意:推的流量不能超过边的容量也不能超过该点余流),并让周围点入队( 注意:s和t不能入队 )
- 2.不断地取队首元素,对队首元素推流
- 3.队列为空时结束算法,t点的余流即为最大流。
- 上述思路是不是看起来很简单,也感觉是完全正确的?
- 但是这个思路有一个问题,就是可能会出现两个点不停地来回推流的情况,一直推到TLE。
- 怎么解决这个问题呢?
- 给每个点一个高度,水只会从高处往低处流。在算法运行时, 不断地对有余流的点更改高度 ,直到这些点全部没有余流为止。
为什么这样就不会出现来回推流的情况了呢?
当两个点开始来回推流时,它们的高度会不断上升,当它们的高度大于s时,会把余流还给s。
所以在开始预流推进前要先把s的高度改为n(点数),免得一开始s周围那些点就急着把余流还给s。
先用图解来表示算法的运行过程:
以下图为例(反向边没画出来)

我们用深蓝色的字表示每个点的余流,用淡蓝色的字表示每个点的高度。
刚开始肯定是:

我们把s点的高度改成6(一共6个点)

然后从往s拼命地灌水,水流会顺着还没满的边向周围的节点流

此时1,3节点还有余流,于是更新1,3号点高度,更新为与它相邻且最低的点的高度+1。

此时我们对1号节点推流。

更新有余流的1,2号节点的高度

为什么1的高度变成了7呢?因为1与s有反向边,因为刚才推过流,反向边有了边权,与1连通的最低的点就是s。
然后是对3节点推流(更新高度是同时进行的只是刚才为了便于理解而分开画了)

然后是2号点推流,注意t点不能更新高度(否则好不容易流过去的水就会往回流了)

4号点推流

2号点推流

3号点推流

除了s和t以外,所有节点都无余流,算法结束,最大流就是t的余流。
完整代码:
1 | // luogu-judger-enable-o2 |
这个预流推进算法相当慢,至于P4722 【模板】最大流 加强版 / 预流推进(毒瘤题)更没希望。
那为什么这么慢,我们还要学呢?
为了下面的讲解做铺垫。
最高标号预流推进(HLPP)
算法步骤:
- 1.先从t到s反向bfs,使每个点有一个初始高度
- 2.从s开始向外推流,将有余流的点放入优先队列
- 3.不断从优先队列里取出高度最高的点进行推流操作
- 4.若推完还有余流,更新高度标号,重新放入优先队列
- 5.当优先队列为空时结束算法,最大流即为t的余流
与基础的余流推进相比的优势:
- 通过bfs预先处理了高度标号,并利用优先队列
(闲着没事可以手写堆)使得每次推流都是高度最高的顶点,以此减少推流的次数和重标号的次数。
优化:
- 和ISAP一样的gap优化,如果某个高度不存在,将所有比该高度高的节点标记为不可到达(使它的高度为n+1,这样就会直接向s推流了)。
代码:
1 |
|
这次终于可以过掉那道毒瘤题了。
总结:
ISAP的时间复杂度为O(n^2m),而HLPP的时间复杂度O(n^2sqrt(m)),但由于HLPP常数过大,在纯随机数据(模板题)下跑得还没有ISAP快,一般来说,用ISAP就够了。
最后感谢钱逸凡大佬的经验共享QWQ