1. 引言
二分图最小点覆盖是图论中的一个经典问题,在任务调度、资源分配、网络流建模等场景中有着广泛应用。本文将从基本概念出发,介绍最小点覆盖与最大匹配之间的等价关系,并给出完整的算法实现与代码示例。
2. 基本概念
在正式讨论算法之前,先明确几个核心定义。
- 二分图:顶点集可以划分为两个互不相交的子集 U 和 V,使得每条边的两个端点分别属于 U 和 V。
- 点覆盖:给定图 G = (V, E),若存在一个顶点集合 S ⊆ V,使得图中每条边至少有一个端点属于 S,则称 S 为图 G 的一个点覆盖。
- 最小点覆盖:在所有点覆盖中,包含顶点数量最少的点覆盖称为最小点覆盖。
对于一般图,求最小点覆盖是 NP 完全问题;但对于二分图,存在多项式时间的高效解法,其理论基础是柯尼希定理。
3. 柯尼希定理
柯尼希定理(König's Theorem)是二分图理论中最重要的结论之一,它建立了最小点覆盖与最大匹配之间的桥梁。
定理内容:在二分图中,最小点覆盖的大小等于最大匹配的大小。
这一定理的意义在于:求二分图的最小点覆盖,可以转化为求二分图的最大匹配,而后者可以通过匈牙利算法在多项式时间内高效求解。
4. 算法思路
基于柯尼希定理,求解二分图最小点覆盖的算法分为两步:
- 求最大匹配:使用匈牙利算法求出二分图的最大匹配。
- 构造最小点覆盖:从最大匹配出发,通过交替路径构造出最小点覆盖集合。
构造最小点覆盖的具体步骤如下:
- 从左侧所有未匹配顶点出发,沿交替路径遍历:先走非匹配边,再走匹配边,交替进行。
- 标记所有被访问到的顶点。
- 最小点覆盖 = 左侧未访问的顶点 ∪ 右侧已访问的顶点。
5. 代码实现
下面给出一个完整的 Java 实现,包含匈牙利算法求最大匹配,以及从匹配结果构造最小点覆盖的过程。
import java.util.*;
public class MinVertexCover {
private int n, m; // 左侧顶点数 n,右侧顶点数 m
private List<Integer>[] adj; // 邻接表
private int[] matchR; // 右侧顶点匹配的左侧顶点
private boolean[] visited; // 右侧顶点是否被访问
public MinVertexCover(int n, int m) {
this.n = n;
this.m = m;
adj = new List[n];
for (int i = 0; i < n; i++) {
adj[i] = new ArrayList<>();
}
matchR = new int[m];
Arrays.fill(matchR, -1);
}
public void addEdge(int u, int v) {
adj[u].add(v);
}
// 匈牙利算法:尝试为左侧顶点 u 寻找匹配
private boolean dfs(int u, boolean[] seen) {
for (int v : adj[u]) {
if (seen[v]) continue;
seen[v] = true;
if (matchR[v] == -1 || dfs(matchR[v], seen)) {
matchR[v] = u;
return true;
}
}
return false;
}
// 求最大匹配
public int maxMatching() {
int result = 0;
for (int u = 0; u < n; u++) {
boolean[] seen = new boolean[m];
if (dfs(u, seen)) {
result++;
}
}
return result;
}
// 构造最小点覆盖
public List<Integer> minVertexCover() {
maxMatching();
int[] matchL = new int[n];
Arrays.fill(matchL, -1);
for (int v = 0; v < m; v++) {
if (matchR[v] != -1) {
matchL[matchR[v]] = v;
}
}
// 从左侧未匹配顶点出发,沿交替路径遍历
boolean[] visitedL = new boolean[n];
boolean[] visitedR = new boolean[m];
Queue<Integer> queue = new LinkedList<>();
for (int u = 0; u < n; u++) {
if (matchL[u] == -1) {
queue.offer(u);
visitedL[u] = true;
}
}
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : adj[u]) {
if (!visitedR[v]) {
visitedR[v] = true;
if (matchR[v] != -1 && !visitedL[matchR[v]]) {
visitedL[matchR[v]] = true;
queue.offer(matchR[v]);
}
}
}
}
// 最小点覆盖 = 左侧未访问 ∪ 右侧已访问
List<Integer> cover = new ArrayList<>();
for (int u = 0; u < n; u++) {
if (!visitedL[u]) cover.add(u);
}
for (int v = 0; v < m; v++) {
if (visitedR[v]) cover.add(n + v);
}
return cover;
}
public static void main(String[] args) {
// 示例:左侧 3 个顶点,右侧 3 个顶点
MinVertexCover solver = new MinVertexCover(3, 3);
solver.addEdge(0, 0);
solver.addEdge(0, 1);
solver.addEdge(1, 1);
solver.addEdge(2, 1);
solver.addEdge(2, 2);
int maxMatch = solver.maxMatching();
System.out.println("最大匹配数: " + maxMatch);
List<Integer> cover = solver.minVertexCover();
System.out.println("最小点覆盖大小: " + cover.size());
System.out.println("最小点覆盖顶点: " + cover);
}
}
6. 复杂度分析
匈牙利算法的时间复杂度为 O(n × e),其中 n 为左侧顶点数,e 为边数。构造最小点覆盖的 BFS 遍历同样为 O(n + e)。因此,整个算法的时间复杂度为 O(n × e),空间复杂度为 O(n + m + e)。
7. 应用场景
二分图最小点覆盖在实际问题中有着广泛的应用:
- 任务调度:在机器与任务的二分图中,最小点覆盖对应最少的机器或任务集合,使得所有依赖关系都被覆盖。
- 资源分配:在资源与需求的二分图中,最小点覆盖可用于确定最少的资源投入。
- 网络流建模:最小点覆盖与最大流、最小割问题密切相关,常用于网络规划与优化。
- 矩阵覆盖问题:在 0-1 矩阵中,用最少的行或列覆盖所有 1 的位置,可建模为二分图最小点覆盖。
8. 总结
二分图最小点覆盖是图论中一个优雅而实用的问题。借助柯尼希定理,我们将它转化为最大匹配问题,并通过匈牙利算法高效求解。理解这一转化过程,不仅有助于掌握二分图的核心性质,也能为解决实际工程中的组合优化问题提供有力工具。

447

被折叠的 条评论
为什么被折叠?



