每日算法 — 使用java实现Hall婚配定理:二分图完美匹配的充要条件

一、问题背景

在资源分配、工作分配和配对问题中,我们经常需要判断:给定一组工人和一组任务,每个工人只能做某些特定的任务,是否存在一种分配方案使得每个工人都有任务做、每个任务只被一个人做?

这就是 二分图完美匹配 的存在性问题。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$ 个子集是不可行的。实际上,我们可以:

  1. 使用最大流/匈牙利算法直接求匹配:如果能找到大小为 $n$ 的匹配,则完美匹配存在
  2. 寻找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定理将组合存在性问题转化为可验证的组合条件,是离散数学中最具洞察力的结果之一。