最大流问题:增广路与 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在每轮中都能有效地逼近最大流。这使得整体时间复杂度不会随流量的大小而剧增。其复杂度仅和图规模有关,很稳定,而最大残余容量的复杂度和流量本身大小相关。
最大流问题:增广路与 Edmonds-Karp 算法的更多相关文章
- HDU3549 Flow Problem(网络流增广路算法)
题目链接. 分析: 网络流增广路算法模板题.http://www.cnblogs.com/tanhehe/p/3234248.html AC代码: #include <iostream> ...
- 一般增广路方法求网络最大流(Ford-Fulkerson算法)
/* Time:2015-6-18 接触网络流好几天了 写的第一个模版————Ford-Fulkerson算法 作用:求解网络最大流 注意:源点是0 汇点是1 如果题目输入的是1到n 请预处理减1 * ...
- hdu 3549 Flow Problem(增广路算法)
题目:http://acm.hdu.edu.cn/showproblem.php?pid=3549 模板题,白书上的代码... #include <iostream> #include & ...
- Power Network (最大流增广路算法模板题)
Time Limit: 2000MS Memory Limit: 32768K Total Submissions: 20754 Accepted: 10872 Description A p ...
- 网络流初步:<最大流>——核心(增广路算法)(模板)
增广路的核心就是引入了反向边,使在进行道路探索选择的时候增加了类似于退路的东西[有一点dp的味道??] 具体操作就是:1.首先使用结构体以及数组链表next[ MAXN ]进行边信息的存储 2.[核心 ...
- 最大流算法之Ford-Fulkerson算法与Edmonds–Karp算法
引子 曾经很多次看过最大流的模板,基础概念什么的也看了很多遍.也曾经用过强者同学的板子,然而却一直不会网络流.虽然曾经尝试过写,然而即使最简单的一种算法也没有写成功过,然后对着强者大神的代码一点一点的 ...
- POJ-1273-Drainage Ditches 朴素增广路
Drainage Ditches Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 70588 Accepted: 2743 ...
- hdu 3549 Flow Problem【最大流增广路入门模板题】
题目:http://acm.hdu.edu.cn/showproblem.php?pid=3549 Flow Problem Time Limit: 5000/5000 MS (Java/Others ...
- 网络流(一)——Edmonds Karp算法
首先是一些关于网络流的术语: 源点:即图的起点. 汇点:即图的终点. 容量:有向边(u,v)允许通过的最大流量. 增广路:一条合法的从源点流向汇点的路径. 网络流问题是在图上进行解决的,我们通常可以将 ...
- 【Luogu】P3381最小费用最大流模板(SPFA找增广路)
题目链接 哈 学会最小费用最大流啦 思路是这样. 首先我们有一个贪心策略.如果我们每次找到单位费用和最短的一条增广路,那么显然我们可以把这条路添加到已有的流量里去——不管这条路的流量是多大,反正它能 ...
随机推荐
- 微信小程序目录结构
一.小程序框架 微信开放平台--小程序框架介绍 小程序的目录结构很清晰,主要由描述整体内容的app和描述具体页面的page组成.一般来说,习惯对小程序的目录结构进行更加清晰的规划,例如将程序种会用到的 ...
- Ansible自动化管理集群,Playbook语法
Ansible配置文件存在优先级的问题 ANSIBLE_CONFIG ansible.cfg 项目目录 .ansible.cfg 当前用户的家目录 /etc/ansible/ansible.cfg A ...
- 基于 Github 平台的 .NET 开源项目模板 - PR 相关
本篇将介绍开源模板的 PR 相关功能配置 赘述 project.yml 使用周期 文件来源: 运行跟目录下的 scanner.bat/sh 生成的. 文件位置: .github\project.yml ...
- 联邦学习开山之作Communication-Efficient Learning of Deep Networks from Decentralized Data
1 介绍 1.1 背景 越来越多的手机和平板电脑成为许多人的主要计算设备.这些设备上强大的传感器(包括摄像头.麦克风和GPS),加上它们经常被携带的事实,意味着它们可以访问前所未有的大量数据,其中大部 ...
- Grafana如何利用Karpenter消除50%的云资源浪费?|落地案例
原文链接: https://grafana.com/blog/2023/11/09/how-grafana-labs-switched-to-karpenter-to-reduce-costs-and ...
- 【一步步开发AI运动小程序】十六、AI运动识别中,如何判断人体站位?
[云智AI运动识别小程序插件],可以为您的小程序,赋于人体检测识别.运动检测识别.姿态识别检测AI能力.本地原生识别引擎,无需依赖任何后台或第三方服务,有着识别速度快.体验佳.扩展性强.集成快.成本低 ...
- cornerstone中raft_server_req_handlers源码解析
1.概述 之前说过raft_server是cornerstone的核心,其中充满了很多req的发送,那么follower收到leader的req会怎么处理呢? 本文就是来解析cornerstone中处 ...
- python get请求传array数组
前言 使用传统的http发get请求时,如果传参为array数组,参数名称为a时,可以这样传值a=1&a=2&a=3,但是当只有一个时,这种方式就不合理了. get请求还有另外一种方式 ...
- 敏捷开发:Sprint Planning 冲刺计划会议详细介绍和用户故事拆分、开发任务细分
Sprint Planning 冲刺计划会议简介 Sprint Planning (冲刺计划会议),又叫规划会议.此会议通过 Scrum 团队的集体沟通讨论,确定接下来的 Sprint 中要完成的待开 ...
- JVM的参数说明
Java虚拟机的参数说明 Java运行时数据区域: 线程共享区:方法区(永久区),堆 线程私有区:虚拟机栈,本地方法栈,程序计数器 JAVA_OPTS="-server -Duser.tim ...