求解最大流的 Ford-Fulkerson 算法

1思路:什么是增广链?


对于一个网络流,如何求解最大流呢?我们当然希望流量越多越好,因此考虑不断增加流量。当然,需要寻找一条从 S 到 T 的路径,其中每一条边的目前流量<容量,即存在增加流量的空间。我们暂且称这样的路径为增广链,也就是有提升流量空间的路径。比如下图中,S → V1 → V3 → T 就是一条增广链



2实现:Ford-Fulkerson


Ford-Fulkerson 算法的做法,也许有同学已经猜到了,就是不断寻找增广链,然后增加流量直到有条边饱和——流量=容量。具体来说,用 BFS 来寻找任意一条增广链,中途记录下增广链上,每个节点的前一个节点;然后从增广链的出发点 S 走向 T,记录下中间 “ 残留 ” 流量(容量 - 流量)的最小值;再重新遍历一遍这条增广链,更新流量。最后的最大流为每次增加的流量累计之和,循环至最后没有增广链为止(无法继续增广)。


如果你兴高采烈地开始敲键盘了,那就犯了一个错误哦。之前举的例子其实是一个反例,由于 BFS 选择的增广链是任意的,我们一开始完全可以选择 S → V1 → V4 → T,然后一起增加流量 1。之后分别选择上面和下面的两条增广链进行增广,所以求出的最大流为 3,不过显然是错误的。正确的应该是除了中间的一条边,其余均满流(流量 = 容量),最大流为 4。




为了解决这个问题,我们发现,边其实可以 “ 反流 ”,即反向边。如果按照原图的流向流量为 1,之后可以把 1 再流回去。所以哪怕第一个找到了 S → V1 → V4 → T 这条增广链,之后可以寻找 S → V2 → V4 → V1 → V3 → T 进行更新,避免这个错误。在实现时,可以存储为双向边,不过需要记录下在原图中的方向。在计算剩余流量(增加流量的空间)时,如果是原来的方向,就返回容量 - 流量;如果是反向边,即倒着流回去流量的,就返回目前的流量。


3代码


说了大致的思路,总要来写代码了吧。Ford - Fulkerson 虽然听起来比较简单,但实际上代码有一点长。主体程序就是一个大循环,通过 BFS 寻找增广链(结果存在 edgeTo[] 里,是从 T 出发往前倒着记录的),如果不存在直接退出;反之找到最小剩余流量,再更新流量。当然,如果不需要保留原来的容量 ca ( capacity ),那么也可以只记录残余流量 ( ca - flow )。


其中,一条边需要记录 from 和 to,代表原图中的方向(判断是否为反向边);fid 和 tid 用于更新流量,为了找回这条边在 map[from] 和 map[to] 里的位置(邻接表才需要的)。get_other() 函数可以求出这条边的另一个节点编号,get_left() 可以求出这条边的残余流量,find() 可以寻找一条增广链并进行更新(增广)。最大流的结果存储在 ans 里,即同步的累加器。



4复杂度分析


对于一个算法,必须有复杂度的分析和估计。先来考虑空间复杂度,如果用邻接矩阵,可能会存不下,但没有存储位置标号等的需要了。所以建议使用邻接表,这样就不太需要担心空间的问题了。


Ford-Fulkerson 算法其实比较慢,仔细算算,每次要跑一遍 BFS、两遍增广链。不过,一共需要找多少条增广链呢?实际上无法给出确切的值(参考之前举的 “ 反例 ”,容量可以很大),但由于容量是有限的,最大流是有限的,那么一定能在有限步之内结束。具体的时间复杂度取决于求出增广链的方法等,今天介绍的方法时间复杂度上限为 O ( n × m × m ),其中 n 为节点数、m 为边数。


5后记


虽然 Ford-Fulkerson 算法并不快,尤其是节点数超过 1000、图是故意出成特殊情况的时候,但是它比较好理解、比较好写代码,是最大流算法的基础思想。同时还可以进一步优化,其他相关算法我们之后再介绍。网络流的题目还是要多练习,才能够找到建图的技巧,最后运用最大流的算法解题。


再来讲一个有趣的 “ 八卦 ” 收尾吧,还记得不久前介绍的 Bellman-Ford 算法 吗?不就是用于求解单源最短路问题嘛,突然发现 Ford 同时出现在两个算法的命名里!确实是同一个 人,真是个了不起的数学家。原来算法之间也有这种联系,是不是很有意思呢?



猜你喜欢

转载自blog.csdn.net/jwg2732/article/details/78516745