引言
网络流问题是图论与运筹学中的经典课题,广泛应用于交通调度、资源分配、图像分割等领域。Edmonds-Karp算法是Ford-Fulkerson方法的一个具体实现,其核心改进在于使用BFS来寻找最短增广路(按边数最少),从而将时间复杂度从指数级优化到多项式级O(VE²)。本文将用Java完整实现该算法,并深入讲解最大流最小割定理的工程意义。
算法原理
残量网络
对于流网络中的每条边(u, v),定义残量容量为:
- 正向边残量:c_f(u, v) = c(u, v) – f(u, v)
- 反向边残量:c_f(v, u) = f(u, v)
其中c为容量,f为当前流量。反向边的存在使得算法可以”撤销”之前的不优决策,这是网络流算法的关键设计。
增广路
增广路是指在残量网络中从源点s到汇点t的一条路径,路径上所有边的残量容量均为正。增广路的流量增加量等于路径上最小残量容量。
Edmonds-Karp核心流程
- 初始化所有边流量为0
- 在残量网络中用BFS寻找s到t的最短增广路
- 若不存在增广路,算法终止
- 否则沿增广路增加流量,更新残量网络
- 重复步骤2-4
最大流最小割定理
网络的最大流值等于最小割的容量。最小割是将顶点集划分为包含s的集合S和包含t的集合T后,从S到T的所有边容量之和的最小值。
Java完整实现
import java.util.*;
/**
* Edmonds-Karp最大流算法实现
* 基于BFS寻找最短增广路,时间复杂度O(V * E^2)
*/
public class EdmondsKarpMaxFlow {
// 边的内部类,支持正向边和反向边
static class Edge {
int from, to; // 起点和终点
int capacity; // 容量
int flow; // 当前流量
Edge reverse; // 反向边引用
Edge(int from, int to, int capacity) {
this.from = from;
this.to = to;
this.capacity = capacity;
this.flow = 0;
}
// 返回残量容量
int residualCapacity() {
return capacity - flow;
}
}
private final int n; // 顶点数量
private final List<Edge>[] graph; // 邻接表
private final int source; // 源点
private final int sink; // 汇点
@SuppressWarnings("unchecked")
public EdmondsKarpMaxFlow(int n, int source, int sink) {
this.n = n;
this.source = source;
this.sink = sink;
this.graph = new ArrayList[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
}
/**
* 添加一条有向边,同时创建反向边
* @param from 起点
* @param to 终点
* @param capacity 容量
*/
public void addEdge(int from, int to, int capacity) {
Edge forward = new Edge(from, to, capacity);
Edge backward = new Edge(to, from, 0); // 反向边初始容量为0
forward.reverse = backward;
backward.reverse = forward;
graph[from].add(forward);
graph[to].add(backward);
}
/**
* 执行Edmonds-Karp算法计算最大流
* @return 从源点到汇点的最大流量
*/
public int maxFlow() {
int totalFlow = 0;
int[] parent = new int[n]; // BFS中记录每个点的前驱边索引
Edge[] parentEdge = new Edge[n]; // 记录前驱边对象
// 不断寻找增广路直到不存在
while (bfs(parent, parentEdge)) {
// 从汇点回溯到源点,找到增广路上的最小残量
int pathFlow = Integer.MAX_VALUE;
for (int v = sink; v != source; v = parentEdge[v].from) {
pathFlow = Math.min(pathFlow, parentEdge[v].residualCapacity());
}
// 沿增广路更新流量
for (int v = sink; v != source; v = parentEdge[v].from) {
Edge e = parentEdge[v];
e.flow += pathFlow; // 正向边增加流量
e.reverse.flow -= pathFlow; // 反向边减少流量(允许撤销)
}
totalFlow += pathFlow;
}
return totalFlow;
}
/**
* BFS寻找从源点到汇点的最短增广路
* @param parent 记录前驱顶点
* @param parentEdge 记录前驱边
* @return 是否存在增广路
*/
private boolean bfs(int[] parent, Edge[] parentEdge) {
Arrays.fill(parent, -1);
Arrays.fill(parentEdge, null);
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
parent[source] = source;
while (!queue.isEmpty()) {
int u = queue.poll();
// 遍历u的所有邻接边
for (Edge e : graph[u]) {
int v = e.to;
// 只考虑残量容量大于0的边,且未访问过的顶点
if (parent[v] == -1 && e.residualCapacity() > 0) {
parent[v] = u;
parentEdge[v] = e;
queue.offer(v);
// 提前终止:找到汇点即可返回
if (v == sink) {
return true;
}
}
}
}
return false;
}
/**
* 获取最小割的S集合(最大流后从源点可达的顶点)
* @return 最小割S侧的顶点集合
*/
public Set<Integer> minCutS() {
Set<Integer> reachable = new HashSet<>();
boolean[] visited = new boolean[n];
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
visited[source] = true;
reachable.add(source);
while (!queue.isEmpty()) {
int u = queue.poll();
for (Edge e : graph[u]) {
if (!visited[e.to] && e.residualCapacity() > 0) {
visited[e.to] = true;
reachable.add(e.to);
queue.offer(e.to);
}
}
}
return reachable;
}
/**
* 打印当前网络的流量状态
*/
public void printFlow() {
System.out.println("网络流量状态:");
for (int u = 0; u < n; u++) {
for (Edge e : graph[u]) {
// 只打印原始正向边(避免重复打印反向边)
if (e.capacity > 0) {
System.out.printf(" 边 %d -> %d: %d / %d%n",
e.from, e.to, e.flow, e.capacity);
}
}
}
}
public static void main(String[] args) {
// 经典测试案例:从0到5的最大流
// 参考CLRS教材中的流网络示例
EdmondsKarpMaxFlow maxFlow = new EdmondsKarpMaxFlow(6, 0, 5);
maxFlow.addEdge(0, 1, 16);
maxFlow.addEdge(0, 2, 13);
maxFlow.addEdge(1, 2, 10);
maxFlow.addEdge(1, 3, 12);
maxFlow.addEdge(2, 1, 4);
maxFlow.addEdge(2, 4, 14);
maxFlow.addEdge(3, 2, 9);
maxFlow.addEdge(3, 5, 20);
maxFlow.addEdge(4, 3, 7);
maxFlow.addEdge(4, 5, 4);
int result = maxFlow.maxFlow();
System.out.println("最大流值: " + result); // 期望输出: 23
maxFlow.printFlow();
Set<Integer> minCut = maxFlow.minCutS();
System.out.println("最小割S集合: " + minCut);
}
}
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(V x E²) | 每次BFS为O(E),最多增广O(VxE)次 |
| 空间复杂度 | O(V + E) | 邻接表存储图结构 |
| 实际性能 | 优于一般Ford-Fulkerson | BFS保证最短增广路,避免瓶颈边反复使用 |
Edmonds-Karp的时间复杂度分析基于一个关键性质:每条边作为瓶颈边的次数最多为O(V)。因为每次增广后,该边在残量网络中的距离至少增加2,而距离上限为V。
应用场景
- 二分图最大匹配:将源点连左侧顶点,右侧顶点连汇点,边容量为1
- 多源多汇问题:添加超级源点和超级汇点
- 带下界网络流:通过差分约束转化为标准最大流
- 最小割问题:直接利用最大流最小割定理求解
总结
Edmonds-Karp算法通过BFS寻找最短增广路,巧妙地规避了一般Ford-Fulkerson方法的最坏情况,是学习和理解网络流问题的最佳入门算法。其核心设计——残量网络与反向边——不仅是该算法的基础,也是Dinic、ISAP等更高级网络流算法的基石。掌握Edmonds-Karp后,读者可以进一步学习时间复杂度为O(E√V)的Dinic算法,以应对更大规模的网络流问题。