引言
二分图最大匹配是图论中的经典问题,在任务分配、资源调度、推荐系统等领域有广泛应用。匈牙利算法是基础的 O(VE) 解法,而 Hopcroft-Karp 算法通过分层 BFS + 多路 DFS 增广,将复杂度优化到 O(E√V),是大规模二分图匹配的首选算法。
本文将用 Java 完整实现 Hopcroft-Karp 算法,讲解其分层思想、多路增广策略与复杂度证明。
问题建模
二分图
图 G = (U, V, E),其中 U 和 V 是不相交的顶点集,E 中的边只连接 U 和 V 中的顶点。
匹配
边集 M ⊆ E,满足任意两条边不共享顶点。最大匹配是基数最大的匹配。
增广路
一条交替路径(匹配边/非匹配边交替),起点和终点都是未匹配点。沿增广路翻转匹配状态可使匹配数 +1。
核心思想
匈牙利算法的瓶颈
每次只找一条增广路,需要 O(E) 时间,最多 V 次增广,总复杂度 O(VE)。
Hopcroft-Karp 的优化
- 分层 BFS:从所有未匹配的 U 顶点同时出发,构建到未匹配 V 顶点的最短增广路层次图
- 多路 DFS 增广:在层次图上同时找出多条顶点不相交的最短增广路,一次性全部增广
- 关键定理:每次增广后,最短增广路长度严格增加,因此最多 √V 轮即可收敛
完整 Java 实现
import java.util.*;
/**
* Hopcroft-Karp 算法实现
* 二分图最大匹配的分层BFS + 多路DFS优化
* 时间复杂度: O(E * sqrt(V))
*/
public class HopcroftKarp {
private final int nU; // U集合大小
private final int nV; // V集合大小
private final List<Integer>[] adj; // U到V的邻接表
// 匹配数组: pairU[u]=v 表示U中u匹配到V中v
private final int[] pairU;
private final int[] pairV;
// BFS层次距离
private final int[] dist;
// 无穷大标记
private static final int INF = Integer.MAX_VALUE;
/**
* 构造函数
* @param nU U集合顶点数 (0 ~ nU-1)
* @param nV V集合顶点数 (0 ~ nV-1)
*/
@SuppressWarnings("unchecked")
public HopcroftKarp(int nU, int nV) {
this.nU = nU;
this.nV = nV;
this.adj = new ArrayList[nU];
for (int i = 0; i < nU; i++) {
adj[i] = new ArrayList<>();
}
this.pairU = new int[nU];
this.pairV = new int[nV];
this.dist = new int[nU];
}
/**
* 添加边 (u属于U, v属于V)
*/
public void addEdge(int u, int v) {
if (u < 0 || u >= nU || v < 0 || v >= nV) {
throw new IllegalArgumentException("顶点越界");
}
adj[u].add(v);
}
/**
* 主算法:返回最大匹配数
*/
public int maxMatching() {
// 初始化匹配数组
Arrays.fill(pairU, -1);
Arrays.fill(pairV, -1);
int matching = 0;
// 每轮BFS构建层次图,然后DFS多路增广
while (bfs()) {
// 对所有未匹配的U顶点尝试DFS增广
for (int u = 0; u < nU; u++) {
if (pairU[u] == -1) {
if (dfs(u)) {
matching++;
}
}
}
}
return matching;
}
/**
* BFS构建层次图
* 从所有未匹配的U顶点出发,计算到各U顶点的最短增广路距离
* 返回是否存在增广路
*/
private boolean bfs() {
ArrayDeque<Integer> queue = new ArrayDeque<>();
// 初始化:所有未匹配的U顶点距离为0,入队
for (int u = 0; u < nU; u++) {
if (pairU[u] == -1) {
dist[u] = 0;
queue.offer(u);
} else {
dist[u] = INF;
}
}
// 设置一个虚拟目标的距离(用于标记是否存在增广路)
// 实际上我们不需要显式NIL节点,只需检查是否能到达未匹配的V顶点
boolean foundAugmentingPath = false;
while (!queue.isEmpty()) {
int u = queue.poll();
// 只扩展最短增广路层次上的边
for (int v : adj[u]) {
int u2 = pairV[v]; // v的匹配点(如果已匹配)
if (u2 != -1 && dist[u2] == INF) {
// v已匹配,沿匹配边走到对应的U顶点
dist[u2] = dist[u] + 1;
queue.offer(u2);
} else if (u2 == -1) {
// v未匹配,找到增广路终点
foundAugmentingPath = true;
}
}
}
return foundAugmentingPath;
}
/**
* DFS在层次图上寻找增广路
* 只走最短增广路层次上的边,避免破坏其他增广路
*/
private boolean dfs(int u) {
for (int v : adj[u]) {
int u2 = pairV[v];
if (u2 == -1 || (dist[u2] == dist[u] + 1 && dfs(u2))) {
// 找到增广路,更新匹配
pairU[u] = v;
pairV[v] = u;
return true;
}
}
// 无法从u出发找到增广路,标记为INF避免重复访问
dist[u] = INF;
return false;
}
/**
* 获取匹配结果
* @return 数组match[u]=v,若u未匹配则为-1
*/
public int[] getMatching() {
return pairU.clone();
}
/**
* 打印匹配结果
*/
public void printMatching() {
System.out.println("最大匹配数: " + countMatching());
System.out.println("匹配详情:");
for (int u = 0; u < nU; u++) {
if (pairU[u] != -1) {
System.out.printf(" U%d ↔ V%d%n", u, pairU[u]);
}
}
}
/**
* 统计匹配数
*/
public int countMatching() {
int count = 0;
for (int v : pairU) {
if (v != -1) count++;
}
return count;
}
/**
* 验证匹配的正确性
*/
public boolean verify() {
Set<String> edges = new HashSet<>();
for (int u = 0; u < nU; u++) {
if (pairU[u] != -1) {
// 检查是否是有效边
if (!adj[u].contains(pairU[u])) return false;
// 检查是否有冲突
String key = pairU[u] + ""; // V顶点只能被匹配一次
if (edges.contains(key)) return false;
edges.add(key);
// 检查双向一致性
if (pairV[pairU[u]] != u) return false;
}
}
return true;
}
/**
* 构建测试用的随机二分图
*/
public static HopcroftKarp randomBipartiteGraph(int nU, int nV, double edgeProb) {
HopcroftKarp hk = new HopcroftKarp(nU, nV);
Random rand = new Random(42);
for (int u = 0; u < nU; u++) {
for (int v = 0; v < nV; v++) {
if (rand.nextDouble() < edgeProb) {
hk.addEdge(u, v);
}
}
}
return hk;
}
/**
* 任务分配示例
*/
public static void taskAssignmentDemo() {
System.out.println("=== 任务分配示例 ===");
// 5个工人,6个任务,每个工人能完成部分任务
HopcroftKarp hk = new HopcroftKarp(5, 6);
// 工人0可以完成任务0,1,2
hk.addEdge(0, 0); hk.addEdge(0, 1); hk.addEdge(0, 2);
// 工人1可以完成任务1,3
hk.addEdge(1, 1); hk.addEdge(1, 3);
// 工人2可以完成任务2,4,5
hk.addEdge(2, 2); hk.addEdge(2, 4); hk.addEdge(2, 5);
// 工人3可以完成任务0,3,5
hk.addEdge(3, 0); hk.addEdge(3, 3); hk.addEdge(3, 5);
// 工人4可以完成任务1,4
hk.addEdge(4, 1); hk.addEdge(4, 4);
int maxMatch = hk.maxMatching();
System.out.println("最大可分配任务数: " + maxMatch);
hk.printMatching();
}
/**
* 与匈牙利算法(DFS版)的性能对比
*/
public static void performanceComparison() {
System.out.println("\n=== 性能对比 ===");
int[] sizes = {100, 200, 500, 1000};
for (int n : sizes) {
HopcroftKarp hk = randomBipartiteGraph(n, n, 0.1);
long start = System.nanoTime();
int match = hk.maxMatching();
long end = System.nanoTime();
System.out.printf("n=%d, 匹配数=%d, 耗时=%.3fms%n",
n, match, (end - start) / 1e6);
}
}
/**
* 测试完全二分图 K_{n,n}
*/
public static void completeBipartiteTest(int n) {
System.out.println("\n=== 完全二分图 K_" + n + "," + n + " ===");
HopcroftKarp hk = new HopcroftKarp(n, n);
for (int u = 0; u < n; u++) {
for (int v = 0; v < n; v++) {
hk.addEdge(u, v);
}
}
long start = System.nanoTime();
int match = hk.maxMatching();
long end = System.nanoTime();
System.out.println("最大匹配: " + match + " (期望: " + n + ")");
System.out.println("验证: " + (hk.verify() ? "通过" : "失败"));
System.out.printf("耗时: %.3fms%n", (end - start) / 1e6);
}
public static void main(String[] args) {
taskAssignmentDemo();
performanceComparison();
completeBipartiteTest(500);
completeBipartiteTest(2000);
}
}
算法解析
为什么分层 BFS 有效
BFS 保证找到的是最短的增广路。关键定理:若最短增广路长度为 k,则一次多路增广后,新的最短增广路长度至少为 k+2。
为什么最多 √V 轮
- 设最大匹配为 M,|M| ≤ min(|U|, |V|)
- 前 √V 轮后,最短增广路长度 > √V
- 剩余未匹配点最多 |M|/√V 个(每条增广路需 √V 条边)
- 因此最多再 |M|/√V ≤ √V 轮
- 总轮数 O(√V),每轮 O(E),总复杂度 O(E√V)
多路增广的正确性
DFS 在层次图上只沿 dist[u2] == dist[u] + 1 的边前进,保证找到的都是同一最短长度的增广路,且顶点不相交(通过 dist[u] = INF 标记已访问)。
复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 匈牙利(DFS) | O(VE) | O(V+E) | 小规模 |
| 匈牙利(BFS) | O(VE) | O(V+E) | 小规模 |
| Hopcroft-Karp | O(E√V) | O(V+E) | 大规模首选 |
| Dinic(网络流) | O(E√V) | O(V+E) | 通用网络流 |
扩展思考
- 带权二分图匹配:使用匈牙利算法或最小费用最大流。
- 一般图最大匹配:使用 Blossom 算法(Edmonds),复杂度 O(V³)。
- 在线匹配:到达的顶点必须立即匹配,使用贪心或 Ranking 算法。
总结
Hopcroft-Karp 算法是二分图匹配的里程碑:通过分层 BFS 找到最短增广路集合,通过多路 DFS 一次性批量增广,将复杂度从 O(VE) 降至 O(E√V)。掌握其核心思想——按层次批量处理——对理解网络流、图匹配乃至并行算法设计都有深远启发。