最大流问题:增广路与 Edmonds-Karp 算法
最大流问题是其中一个经典的图论问题,其目标是在一个流网络中计算从源点到汇点的最大流量。流网络由节点和边组成,每条边都有一个容量,表示该边所能承载的最大流量。
最大流问题
通常来说,最大流问题仅在有向图上考虑,允许成环,且不考虑重边和自环。在数学上,流网络可以表示为一个有向图 $ G = (V, E) $,其中:
- $ V $ 是图的节点集合,包含源点 $ s $ 和汇点 $ t $,以及若干中间节点。
- $ E $ 是图的边集合,每条边 $ (u, v) \in E $ 具有一个非负容量 $ c(u, v) $,表示单位时间内通过该边的最大流量。
最大流问题的目标是找到从源点 $ s $ 到汇点 $ t $ 的最大流量 $ f $。具体地,我们需要满足以下条件:
1. 流量守恒条件
对于图中的每个中间节点 $ v $(即 $ v \neq s $ 且 $ v \neq t $),流量必须满足流量守恒:流入节点 $ v $ 的流量等于流出节点 $ v $ 的流量。数学表达式为:
这意味着每个中间节点的流量是平衡的,即不存留流量。
2. 源点流出
源点 $ s $ 的流量没有任何限制,可以向网络中流出任意数量的流量,因此流出源点的流量等于最大流:
其中 $ f_{\text{max}} $ 为从源点 $ s $ 到汇点 $ t $ 的最大流量。
3. 汇点流入
汇点 $ t $ 的流量也没有限制,但流入汇点的流量最终应也等于最大流。即我们要的答案,越大越好:
4. 容量限制
流量必须受到容量的限制,即每条边的流量不能超过其容量:
流量的大小 $ f(u, v) $ 需要满足每条边的容量约束。
最大流问题可以通过以下最优化问题来表达:
这个优化问题的目标是最大化从源点 $ s $ 流向汇点 $ t $ 的流量,同时确保流量守恒和容量限制。
假定下图的有向边是可以流动的最大人数,那么有多少人最终可以从武汉到上海呢?这道题目的答案是$ 8$。
最大流与最小割
要了解最大流的求解问题,一个经典的理论基础就是最大流最小割定理。
一个流网络的割是将图中的所有节点分成两个不相交的集合,源点属于一个集合,汇点属于另一个集合。割的容量是指从源点集合到汇点集合的所有边的容量总和。
一方面,最大流 $ f_{max} \leq $ 最小割容量。设有一个割 $ (S, T) $,其中 $ S $ 包含源点 $ s $, $ T $ 包含汇点 $ t $。割的容量是所有从 $ S $ 到 $ T $ 的边的容量之和。由于网络中的流量只能沿着边传递,所以流量也只能经过这些割边。 因此,任何从源到汇的流量都不能超过这个割的容量。换句话说,流量的总和最多等于最小割的容量,
另一方面,最小割的容量 $ \text{割的容量} \geq f_{max} $。在最大流 $ f_{max} $ 达到时,流网络的每条边都已经充分利用,流量从源点 $ s $ 出发,经过不同的路径最终到达汇点 $ t $。如果我们构造一个“残量网络”,其中只考虑当前流量下剩余容量大于零的边,并通过分割将网络分为两个部分 $ S $ 和 $ T $(其中 $ S $ 包含源点,$ T $ 包含汇点),割的容量实际上就是从 $ S $ 到 $ T $ 的所有边的容量之和。在最大流达到时,这个割的容量等于最大流的流量,因为流量的传输和割的“限制”是相对应的。最小割容量无法小于最大流量,否则流量就会超过割的容量,从而导致不可能传输这些流量。
最大流最小割定理是最大流问题的一个重要结果,它揭示了流量与网络中“割”的关系。具体来说,这一定理告诉我们,在任何流网络中,最大流等于最小割的容量。换句话说,最大流问题的解可以通过最小割的大小来描述。定理可以使用数学知识证明。
这意味着最大流的大小等于将源点与汇点分开的“最小割”的容量。这个定理为求解最大流问题提供了理论依据,也指导了我们在实际问题中如何去寻找最优解。在最大流问题中,我们试图通过增广路径逐步提升流量,直到无法再找到增广路径为止。而增广路径的存在性与最小割的结构息息相关。当最大流形成时,所有源点向汇点的割边上都充满流量,加起来正等于最大流值(也是最小割值)。
上图的最小割也是 \(8\)。直接割掉源点伸出的两条边即可。
增广路与残差网络
在求解最大流问题时,增广路径是一个核心概念。增广路径是从源点到汇点的一条路径,其中每条边的剩余容量(即边的原始容量减去当前流量)大于零。
既然如此,我们只要找到一条源点到汇点间尚有空余流量的通路(即增广路),就可以增加最大流。通过反复找寻增广路径增大流量,直到找不到增广路,我们可以逐步提升网络中的流量到最大值。
为什么不断寻找增广路径直到不存在来求解最大流是正确的?
这里的核心原因在于最大流最小割定理。该定理表明,最大流等于最小割的容量,也就是说,在流网络中的最大流量最终将受到最小割的限制。这个定理告诉我们,最大流的计算与最小割之间有着紧密的关系,而增广路径法正是基于这一理论来逐步寻找最大流。数学上可以证明,增广的正确性和最大流最小割定理等价。
当我们通过增广路径逐步增加流量时,实际上是在不断减少源点到汇点之间的可用容量。每次增广路径的更新都会改变网络中可用的流量,并可能“改变”最小割的结构。随着增广路径的不断增多,最终流量将无法再增加,即无法再找到增广路径时,算法将到达最大流,这时剩余的容量正好等于最小割的容量。这种通过不断寻找增广路径求解最大流的办法,叫做 Ford–Fulkerson 算法。
增广路径与残差网络
为了有效地实现增广路径的寻找,我们需要借助残差网络的概念。残差网络是原图的一个变种,它反映了当前流量分配后,网络中仍然可以承载的流量。残差网络的构造方法如下:
- 对于每条原图中的边 $ (u, v) $,如果 $ f(u, v) $ 是从 $ u $ 到 $ v $ 当前流量,则残差容量为 $ c(u, v) - f(u, v) $。
- 同时,对于每条边 $ (u, v) $,还会引入一个反向边 $ (v, u) $,其残差容量为 $ f(u, v) $,即反向流量。
残差网络的特点之一是,它不仅包含了正向边的剩余容量,还包含了反向边的流量容量。正向边表示本增广路已被使用后的剩余流量,反向边表示已找到的增广路在未来可能被反悔撤回。这是增广路径法正确性的关键,无论用怎样的顺序寻找增广路,最终都可以得到正确结果。
残差网络是不断寻找增广路的中间产物,当我们在残差网络中寻找增广路径时,实际上是在找从源点到汇点的一条路径,使得沿途的每一条边都具有正的残差容量。每找到一条增广路径后,我们就沿着这条路径增加流量,同时更新残差网络中的正向边和反向边的容量,又得到一个残差网络。当增广路径不再存在时,意味着网络中没有可行的路径来进一步增加流量,此时最大流已经达到,同时增广路径不再存在也意味着此时的残差网络不再联通,最小割也被达成。
下面的图最大流是 \(20000\)。即使一开始不幸选中 \(1 → 2 → 3 → 4\),也可以由反向边反悔,再次选择 \(1 → 3 → 2 → 4\) 抵消,反复迭代后最终仍然可得正确结果。
Edmonds-Karp 算法
在求解最大流问题时,我们已经知道要不断寻找增广路径。那么用什么策略选择增广路径就是一个问题,选择直接影响到算法的效率和复杂度。我们可以采用多种策略来寻找增广路径,每种策略在不同场景下可能表现出不同的效率。然而,EK算法(Edmonds-Karp算法)选择了广度优先搜索(BFS)来寻找最短的增广路径,这一策略已成为最流行的方式。
EK 算法的核心思想是通过BFS寻找最短增广路径,并不断更新流量和残差网络,直到没有增广路径为止。以下是具体实现步骤:
-
初始化:
- 设置流量 $ f(u, v) = 0 $ 对所有边初始化流量。
- 构建残差图 $ G_f $,每条边的残差容量为其原始容量 $ c(u, v) $。
-
寻找增广路径:
- 使用BFS在残差图中寻找一条从源点 $ s $ 到汇点 $ t $ 的增广路径。这条路径由若干边组成,其中每条边的残差容量都大于零。
-
更新流量与残差图:
- 找到增广路径后,计算该路径上的最小残差容量 $ \Delta $。
- 沿着增广路径增加流量 $ \Delta $,并更新残差网络:
- 对于每条正向边 $ (u, v) $,减少其残差容量 $ c(u, v) $ 并增加反向边 $ (v, u) $ 的流量。
- 对于每条反向边 $ (v, u) $,增加其残差容量。
-
重复步骤 2 和 3:
- 重复以上过程,直到找不到增广路径为止。此时,当前的流量即为最大流。
-
输出结果:
- 最终返回从源点到汇点的最大流量,即为最大流问题的解。
#include <iostream> #include <vector> #include <queue> #include <climits> using namespace std; // 定义一个边的结构体 struct Edge { int v, capacity, flow; Edge(int v, int capacity) : v(v), capacity(capacity), flow(0) {} }; // 图的邻接链表表示 class Graph { public: int V; // 图的顶点数 vector<vector<Edge>> adj; // 邻接链表表示的图 Graph(int V) { this->V = V; adj.resize(V); } // 添加一条边 void addEdge(int u, int v, int capacity) { adj[u].emplace_back(v, capacity); // 正向边 adj[v].emplace_back(u, 0); // 反向边,初始容量为0 } // 使用BFS查找增广路径 bool BFS(int s, int t, vector<int>& parent) { // 记录每个节点的访问状态 vector<bool> visited(V, false); queue<int> q; q.push(s); visited[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); for (auto& edge : adj[u]) { int v = edge.v; if (!visited[v] && edge.capacity > edge.flow) { // 只考虑容量大于流量的边 visited[v] = true; parent[v] = u; // 记录路径 if (v == t) return true; // 找到增广路径 q.push(v); } } } return false; } // 寻找增广路径并更新流量 int edmondsKarp(int s, int t) { int maxFlow = 0; vector<int> parent(V); // 记录增广路径的父节点 // 不断找到增广路径并更新流量 while (BFS(s, t, parent)) { // 找到一条增广路径 int pathFlow = INT_MAX; for (int v = t; v != s; v = parent[v]) { int u = parent[v]; // 找到该边的剩余容量 for (auto& edge : adj[u]) { if (edge.v == v) { pathFlow = min(pathFlow, edge.capacity - edge.flow); break; } } } // 更新流量 for (int v = t; v != s; v = parent[v]) { int u = parent[v]; for (auto& edge : adj[u]) { if (edge.v == v) { edge.flow += pathFlow; // 更新正向边流量 break; } } // 更新反向边流量 for (auto& edge : adj[v]) { if (edge.v == u) { edge.flow -= pathFlow; // 反向流量 break; } } } maxFlow += pathFlow; // 增加流量 } return maxFlow; } }; int main() { // 输入节点数、边数 int V, E; cin >> V >> E; Graph g(V); // 输入每条边的信息:u, v, capacity for (int i = 0; i < E; i++) { int u, v, w; cin >> u >> v >> w; g.addEdge(u, v, w); // 添加边 } int s, t; cin >> s >> t; // 输入源点s和汇点t // 计算最大流 int maxFlow = g.edmondsKarp(s, t); cout << "最大流量为: " << maxFlow << endl; return 0; }
时间复杂度分析
每次BFS的时间复杂度是 $ O(E) $,因为BFS需要遍历图中的所有边。每次BFS都用于找到一条增广路径,并对残差图进行更新,包含对流量的调整以及反向边的更新。
增广的轮数上界是我们分析时间复杂度时的关键。对于每一轮增广,至少有一条边的流量被增大了。增广路径的轮数受限于图中的残差容量的变化。具体来说,增广路径最多会进行 $ O(V \cdot E) $ 轮,严谨的证明较复杂,大致理解来说,每次增广至少会减少一条边的剩余容量。每次增广的过程中,某条路径上的边都会调整其流量,而每次增广路径的选择都会导致残差网络中某些边的容量减少。在最坏情况下,增广路径的数目不会超过图中边数和节点数的乘积。具体地,这个上界为 $ O(V \cdot E) $,因为每次增广最多减少一个单位的流量,而图中最多有 $ E $ 条边,每条边可能会参与多轮增广(在某些情况下,可能经过多次流量增大才会被“消耗”完)。每个节点最多会经历 $ O(E) $ 轮增广。
因此,增广路径的总轮数的上界是 $ O(V \cdot E) $。这意味着尽管每轮BFS的时间复杂度为 $ O(E) $,但在最坏情况下,总的增广轮数是 $ O(V \cdot E) $,因此EK算法的总时间复杂度为:
这是一个保守宽松的上界。
为什么 EK 算法用 BFS 寻找最短增广路径?
EK 算法是 Ford–Fulkerson 算法最经典的实现之一。为什么要总是选择最短增广路径?为什么不是其他策略呢(比如每次寻找具有最大残余容量的增广路)?
使用BFS时,每次增广路径的寻找都会保证找到图中最短的增广路径(即路径中的边数最少)。这意味着每次增广都会尽量提高流量,避免不必要的流量“浪费”,而相对于DFS可能导致的“长路径”,BFS能够较为均匀地分配流量,提高了每次增广的效率。。由于每次增广的路径长度都相对较短,BFS在每轮中都能有效地逼近最大流。这使得整体时间复杂度不会随流量的大小而剧增。其复杂度仅和图规模有关,很稳定。相比之下,最大残余容量策略的复杂度和流量本身大小相关。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤