一、问题背景
在资源分配、工作分配和配对问题中,我们经常需要判断:给定一组工人和一组任务,每个工人只能做某些特定的任务,是否存在一种分配方案使得每个工人都有任务做、每个任务只被一个人做?
这就是 二分图完美匹配 的存在性问题。Hall婚配定理 给出了完美匹配的充要条件,是组合数学中最优美的结果之一。
二、核心思想
2.1 Hall条件
设二分图 $G = (X \cup Y, E)$,其中 $|X| = |Y| = n$。$G$ 存在完美匹配当且仅当:
对于 $X$ 的任意子集 $S$,其邻域 $N(S)$ 满足:
$$|N(S)| \geq |S|$$
2.2 构造性验证
虽然Hall条件给出了判定准则,但直接检查所有 $2^n$ 个子集是不可行的。实际上,我们可以:
- 使用最大流/匈牙利算法直接求匹配:如果能找到大小为 $n$ 的匹配,则完美匹配存在
- 寻找Hall缺陷集:如果完美匹配不存在,找出一个违反Hall条件的子集 $S$ 作为证据
三、Java实现
3.1 二分图表示与基础操作
import java.util.*;
/**
* Hall婚配定理实现
* 用于判定二分图完美匹配的存在性,并在不存在时给出反例
*/
public class HallMarriageTheorem {
private final int nLeft; // 左部点数量
private final int nRight; // 右部点数量
private final List<Integer>[] adj; // 左部点的邻接表
@SuppressWarnings("unchecked")
public HallMarriageTheorem(int nLeft, int nRight) {
this.nLeft = nLeft;
this.nRight = nRight;
this.adj = new ArrayList[nLeft];
for (int i = 0; i < nLeft; i++) {
adj[i] = new ArrayList<>();
}
}
public void addEdge(int left, int right) {
adj[left].add(right);
}
/**
* 计算某个左部点子集的邻域大小
*/
public int neighborhoodSize(Set<Integer> subset) {
Set<Integer> neighbors = new HashSet<>();
for (int u : subset) {
neighbors.addAll(adj[u]);
}
return neighbors.size();
}
3.2 匈牙利算法求最大匹配
/**
* 使用匈牙利算法求最大匹配
* @return matchR[v] 表示右部点 v 匹配的左部点,-1表示未匹配
*/
public int[] maxMatching() {
int[] matchR = new int[nRight];
Arrays.fill(matchR, -1);
for (int u = 0; u < nLeft; u++) {
boolean[] visited = new boolean[nRight];
dfs(u, matchR, visited);
}
return matchR;
}
private boolean dfs(int u, int[] matchR, boolean[] visited) {
for (int v : adj[u]) {
if (visited[v]) continue;
visited[v] = true;
if (matchR[v] == -1 || dfs(matchR[v], matchR, visited)) {
matchR[v] = u;
return true;
}
}
return false;
}
/**
* 检查是否存在完美匹配
*/
public boolean hasPerfectMatching() {
if (nLeft != nRight) return false;
int[] matchR = maxMatching();
int count = 0;
for (int v : matchR) {
if (v != -1) count++;
}
return count == nLeft;
}
3.3 Hall缺陷集查找(当完美匹配不存在时)
/**
* 当完美匹配不存在时,找出一个违反Hall条件的子集
* 这可以作为"不存在完美匹配"的构造性证明
*
* 算法思路:
* 1. 求最大匹配
* 2. 从所有未匹配的左部点出发,沿交错路径(未匹配边->匹配边->未匹配边...)进行BFS
* 3. 访问到的左部点集合 Z,则 S = Z 就是一个Hall缺陷集
*/
public Set<Integer> findHallDeficiency() {
int[] matchR = maxMatching();
int[] matchL = new int[nLeft];
Arrays.fill(matchL, -1);
for (int v = 0; v < nRight; v++) {
if (matchR[v] != -1) {
matchL[matchR[v]] = v;
}
}
// 从所有未匹配的左部点出发BFS
boolean[] leftVisited = new boolean[nLeft];
boolean[] rightVisited = new boolean[nRight];
Queue<Integer> queue = new LinkedList<>();
for (int u = 0; u < nLeft; u++) {
if (matchL[u] == -1) {
queue.offer(u);
leftVisited[u] = true;
}
}
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : adj[u]) {
if (rightVisited[v]) continue;
// 只能沿非匹配边走到右部
if (matchL[u] != v) {
rightVisited[v] = true;
// 从右部沿匹配边回到左部
if (matchR[v] != -1 && !leftVisited[matchR[v]]) {
leftVisited[matchR[v]] = true;
queue.offer(matchR[v]);
}
}
}
}
// Hall缺陷集:所有访问过的左部点
Set<Integer> deficiency = new HashSet<>();
for (int i = 0; i < nLeft; i++) {
if (leftVisited[i]) {
deficiency.add(i);
}
}
return deficiency;
}
/**
* 验证给定的集合是否真的是Hall缺陷集
*/
public boolean verifyHallDeficiency(Set<Integer> subset) {
return neighborhoodSize(subset) < subset.size();
}
3.4 最大流方法(用于带容量的情况)
/**
* 使用Dinic最大流算法求完美匹配
* 适用于更一般的带容量二分图匹配
*/
public boolean hasPerfectMatchingByMaxFlow() {
int nodes = nLeft + nRight + 2;
int source = nLeft + nRight;
int sink = nLeft + nRight + 1;
Dinic dinic = new Dinic(nodes, source, sink);
// 源点到左部
for (int u = 0; u < nLeft; u++) {
dinic.addEdge(source, u, 1);
}
// 左部到右部
for (int u = 0; u < nLeft; u++) {
for (int v : adj[u]) {
dinic.addEdge(u, nLeft + v, 1);
}
}
// 右部到汇点
for (int v = 0; v < nRight; v++) {
dinic.addEdge(nLeft + v, sink, 1);
}
long flow = dinic.maxFlow();
return flow == nLeft && flow == nRight;
}
/**
* Dinic最大流实现
*/
static class Dinic {
static class Edge {
int to, rev;
long cap;
Edge(int to, long cap, int rev) {
this.to = to; this.cap = cap; this.rev = rev;
}
}
List<Edge>[] graph;
int[] level, iter;
int source, sink;
@SuppressWarnings("unchecked")
Dinic(int n, int source, int sink) {
this.source = source; this.sink = sink;
graph = new ArrayList[n];
for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
level = new int[n];
iter = new int[n];
}
void addEdge(int from, int to, long cap) {
graph[from].add(new Edge(to, cap, graph[to].size()));
graph[to].add(new Edge(from, 0, graph[from].size() - 1));
}
void bfs() {
Arrays.fill(level, -1);
Queue<Integer> q = new LinkedList<>();
level[source] = 0;
q.offer(source);
while (!q.isEmpty()) {
int v = q.poll();
for (Edge e : graph[v]) {
if (e.cap > 0 && level[e.to] < 0) {
level[e.to] = level[v] + 1;
q.offer(e.to);
}
}
}
}
long dfs(int v, long f) {
if (v == sink) return f;
for (int i = iter[v]; i < graph[v].size(); i++) {
Edge e = graph[v].get(i);
if (e.cap > 0 && level[v] < level[e.to]) {
long d = dfs(e.to, Math.min(f, e.cap));
if (d > 0) {
e.cap -= d;
graph[e.to].get(e.rev).cap += d;
return d;
}
}
iter[v]++;
}
return 0;
}
long maxFlow() {
long flow = 0;
while (true) {
bfs();
if (level[sink] < 0) return flow;
Arrays.fill(iter, 0);
long f;
while ((f = dfs(source, Long.MAX_VALUE)) > 0) {
flow += f;
}
}
}
}
3.5 完整测试与演示
public static void main(String[] args) {
System.out.println("=== Hall婚配定理演示 ===\n");
// 示例1:存在完美匹配
System.out.println("示例1:存在完美匹配");
HallMarriageTheorem h1 = new HallMarriageTheorem(4, 4);
h1.addEdge(0, 0); h1.addEdge(0, 1);
h1.addEdge(1, 1); h1.addEdge(1, 2);
h1.addEdge(2, 2); h1.addEdge(2, 3);
h1.addEdge(3, 0); h1.addEdge(3, 3);
boolean perfect1 = h1.hasPerfectMatching();
System.out.println("完美匹配存在?" + perfect1);
if (!perfect1) {
Set<Integer> def = h1.findHallDeficiency();
System.out.println("Hall缺陷集:" + def);
System.out.println("邻域大小:" + h1.neighborhoodSize(def));
}
// 示例2:不存在完美匹配
System.out.println("\n示例2:不存在完美匹配");
HallMarriageTheorem h2 = new HallMarriageTheorem(4, 4);
h2.addEdge(0, 0);
h2.addEdge(1, 0);
h2.addEdge(2, 0);
h2.addEdge(3, 1); h2.addEdge(3, 2); h2.addEdge(3, 3);
boolean perfect2 = h2.hasPerfectMatching();
System.out.println("完美匹配存在?" + perfect2);
if (!perfect2) {
Set<Integer> def = h2.findHallDeficiency();
System.out.println("Hall缺陷集:" + def);
System.out.println("缺陷集大小:" + def.size());
System.out.println("邻域大小:" + h2.neighborhoodSize(def));
System.out.println("验证:|N(S)| = " + h2.neighborhoodSize(def) +
" < |S| = " + def.size() +
" => " + h2.verifyHallDeficiency(def));
}
// 示例3:工作分配问题
System.out.println("\n示例3:工作分配问题");
String[] workers = {"Alice", "Bob", "Carol", "David"};
String[] tasks = {"前端", "后端", "测试", "运维"};
HallMarriageTheorem h3 = new HallMarriageTheorem(4, 4);
// Alice可以做前端、后端
h3.addEdge(0, 0); h3.addEdge(0, 1);
// Bob可以做后端、测试
h3.addEdge(1, 1); h3.addEdge(1, 2);
// Carol可以做测试、运维
h3.addEdge(2, 2); h3.addEdge(2, 3);
// David可以做前端、运维
h3.addEdge(3, 0); h3.addEdge(3, 3);
boolean perfect3 = h3.hasPerfectMatchingByMaxFlow();
System.out.println("能否完成分配?" + perfect3);
int[] match = h3.maxMatching();
if (perfect3) {
System.out.println("分配方案:");
for (int v = 0; v < 4; v++) {
if (match[v] != -1) {
System.out.println(" " + workers[match[v]] + " -> " + tasks[v]);
}
}
}
}
}
四、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 匈牙利算法 | $O(VE)$ | $O(V)$ |
| Dinic最大流 | $O(E\sqrt{V}})$(单位容量) | $O(E)$ |
| Hall缺陷集查找 | $O(VE)$ | $O(V)$ |
| 全子集Hall验证 | $O(2^n \cdot E)$ | $O(V)$ |
五、扩展应用
- 正则二分图:$k$-正则二分图必然存在完美匹配
- b-匹配:每个点有容量限制的广义匹配
- 稳定婚姻问题:Gale-Shapley算法的理论基础
- 网络流中的整数性定理:二分图匹配的整数解保证
Hall定理将组合存在性问题转化为可验证的组合条件,是离散数学中最具洞察力的结果之一。