知识梳理
在「初识最大流问题」中,我们了解了什么是流网络模型、什么是最大流问题、以及在流网络中 的增广路(Augmenting Path)概念;
在「Ford-Fulkerson 最大流求解方法」中,我们学习了 Ford-Fulkerson 的最大流问题求解方法和思路:不断的深度优先搜索,直到没有增广路为止则获得最大流;
在「二分匹配的最大流思维」中,通过增加超级源和超级汇来修改二分图,从而将二分匹配问题转换成了最大流问题,最后通过 Ford-Fulkerson 方法解决。
以上三篇前导文章都是在认识和使用最大流这种问题模型,从而进行一些算法思考。但是我们始终没有关心 Ford-Fulkerson 方法的时间复杂度问题。
这篇文章会讲述一个求解最大流问题的 EK 算法,从而优化在某些场景下最大流问题的求解效率。
Ford-Fulkerson 方法有什么问题?
我们知道,在之前讨论的图中,根据 Ford-Fulkerson 方法,我们采用深度优先搜索(下文简称 DFS),不断的去寻找查询增广路,从而增加超级汇点的流量。先来复习一下 Ford-Fulkerson 方法的算法流程:
使用 DFS 搜索 出一条增广路;
在这条路径中所有的边的容量减去这条增广路的流量 f,并建立容量为 f 的反向边;
返回操作一,直到没有增广路;
在这个算法流程中,为将 “使用 DFS” 进行了加粗,你一定察觉到一些端倪。我们来从这个角度来思考一下:
假设有一个网络流如上图所示,我们可以一眼看出最大流是 99。但是在我们代码中使用 Fold-Fulkerson 算法进行查找增广路的过程中,由于根据标号进行搜索,所以一定会先找到 S → A → C → .... → D → E → T 这条增广路。于是我们就浪费了很多开销。
其实我们在这个问题中,只要找到 S → B → E → T 这条增广路,就可以将 T 的入度达到满流状态,后续也就直接结束了。
算法导论上给出的最坏情况分析
如果有一个图,某一条边是一个“噪声边”(所谓“噪声”就是指它在最终的结果中是没有对汇点进行增广的,也就是没有贡献流量的),它的容量很少,并且它在 DFS 搜索中,位置十分靠前,每一次都优先搜到了这一条增广路,那么在每一个二次搜索增广路的时候,都会去抵消它的流量,通过反向边完成一次真实的增广操作。 这样问题就十分严重了。我举一个例子:
上面这个图,我们看一眼就知道它的结果是 s → 0 → t 和 s → 1 → t 这两个增广路贡献的流量和 199
,但是由于 0 → 1 这条边的序号十分靠前,所以每次在进行搜索增广路的过程中,就会优先使用 S → A → B → T 这条边;然后在第二次选择增广路时,我们选择了 S → B → A → T ,如此这样反复,我们发现每一次找到增广路,只增加了 1
个单位的流量,所以如此反复 199
次才能完成最大流算法的计算。
我们用动图来描述一下这个场景:
结合代码,我们来看看到底进行了多少次的增广操作:
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;
#define INF 0x3f3f3f3f
const int MAX_V = 1e4 + 5;
struct Edge {
int to, cap, rev;
Edge(int _to, int _cap, int _rev): to(_to), cap(_cap), rev(_rev) {}
};
// 邻接表记录点和其相连的边
// 比如节点 1 的所有出度边集 G[1]
vector<Edge> G[MAX_V];
void add_edge(int from, int to, int cap) {
G[from].push_back(Edge(to, cap, G[to].size()));
G[to].push_back(Edge(from, 0, G[from].size() - 1));
}
bool used[MAX_V];
int dfs(int c, int t, int f) {
if (c == t) {
return f;
}
used[c] = true;
for (int i = 0; i < G[c].size(); ++ i) {
Edge &e = G[c][i];
if (!used[e.to] && e.cap > 0) {
int d = dfs(e.to, t, min(f, e.cap));
if (d > 0) {
e.cap -= d;
G[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}
int max_flow(int s, int t) {
int flow = 0;
int cnt = 0;
for (;;) {
memset(used, 0, sizeof(used));
int f = dfs(s, t, INF);
cnt += 1;
if (f == 0) {
cout << cnt << endl; // 5 - 也就是说只进行了 5 次增广路查询
return flow;
}
flow += f;
}
}
int main() {
// 0: S点
// 1: A点
// 2: B点
// 3: T点
add_edge(0, 1, 99);
add_edge(0, 2, 100);
add_edge(1, 2, 1);
add_edge(1, 3, 100);
add_edge(2, 3, 100);
int ret = max_flow(0, 3);
cout << ret << endl; // 199
}
但我们发现,即使先搜索了 S → A → B → T 这条增广路,也不会出现这种最坏情况。原因是因为我们所实现的 Ford-Fulkerson 方法是使用 DFS 深度优先搜索查找增广路,在实现中有这句:
int d = dfs(e.to, t, min(f, e.cap));
这个 DFS 中会有回溯流程,也就是说,当我们找到 S → A → B → T 之后,对所有边的容量减 1
,此时回溯到了 A 点,则又会继续查找 S → A → T 这条增广路。所以我们并不能看到《算法导论》中讨论的这种最差情况。
虽然这种极限情况是无法得到的,但是我们也知道了 传统的 Ford-Fulkerson 方法还是存在优化的可能。
基于 DFS 的 FF 方法复杂度分析
从上面这里例子,你已经发现了,当基于 DFS 的 FF 算法的最差情况复杂度是和最大流相关的。假设我们有 E
条边并且最大流是 F
,每次 DFS 查增广路则需要 O(E)
的复杂度,当最大流是 E
的时候,我们要进行最多 E
次的增广路查找。
所以基于 DFS 的 FF 算法的时间复杂度是: