求解最大流的 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 同时出现在两个算法的命名里!确实是同一个 人,真是个了不起的数学家。原来算法之间也有这种联系,是不是很有意思呢?



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Ford-Fulkerson算法求解最大流问题的一种经典算法。以下是一个基于增广路思想的Ford-Fulkerson算法的代码: ``` // 基于邻接矩阵的Ford-Fulkerson算法实现 #include <iostream> #include <queue> #include <cstring> using namespace std; const int MAXN = 100; // 最大顶点数 const int INF = 0x3f3f3f3f; // 表示无穷大 int n, m; // n表示顶点数,m表示边数 int s, t; // s表示源点,t表示汇点 int cap[MAXN][MAXN]; // 表示容量 int flow[MAXN][MAXN]; // 表示流量 int pre[MAXN]; // 表示前驱节点 int bfs() { memset(pre, -1, sizeof(pre)); // 初始化前驱节点数组 queue<int> q; q.push(s); pre[s] = -2; while (!q.empty()) { int u = q.front(); q.pop(); for (int v = 0; v < n; ++v) { if (pre[v] == -1 && cap[u][v] > flow[u][v]) { pre[v] = u; if (v == t) return 1; q.push(v); } } } return 0; } int maxFlow() { int ans = 0; while (bfs()) { int minflow = INF; for (int u = t; u != s; u = pre[u]) { int v = pre[u]; minflow = min(minflow, cap[v][u] - flow[v][u]); } for (int u = t; u != s; u = pre[u]) { int v = pre[u]; flow[v][u] += minflow; flow[u][v] -= minflow; } ans += minflow; } return ans; } int main() { cin >> n >> m >> s >> t; memset(cap, 0, sizeof(cap)); memset(flow, 0, sizeof(flow)); for (int i = 0; i < m; ++i) { int u, v, c; cin >> u >> v >> c; cap[u][v] += c; // 注意有可能存在重边 } cout << maxFlow() << endl; return 0; } ``` 算法思路: 1. 初始化流量为0; 2. 在剩余容量大于0的情况下,寻找增广路: - 从源点s开始,使用BFS寻找一条增广路; - 如果找到增广路,计算增广路上的最小剩余容量minflow,更新流量; 3. 最大流就是所有增广路上的最小剩余容量之和。 其中,增广路的定义是指从源点到汇点路径上,剩余容量均大于0的路径。在Ford-Fulkerson算法中,每次都需要寻找一条增广路来更新流量,直到无法再找到增广路为止。这个过程中,每次找到的增广路都可以使得流量增加,因此最终的流量是不断增加的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值