每日算法 — 使用java实现Hopcroft-Karp:二分图最大匹配的分层BFS优化

算法概述

Hopcroft-Karp算法是解决二分图最大匹配问题的经典算法,由John Hopcroft和Richard Karp于1973年提出。该算法在标准匈牙利算法(DFS增广路)的基础上引入了分层BFS思想,将时间复杂度从O(VE)优化到O(E√V),在处理大规模二分图时具有显著优势。

本文将深入讲解Hopcroft-Karp算法的核心思想,并给出完整的Java实现,包含图的构建、分层BFS、DFS增广以及复杂度分析。

核心思想

二分图与匹配

二分图G=(U,V,E)的顶点集可划分为两个不相交的子集U和V,使得每条边都连接U中的一个顶点和V中的一个顶点。匹配是指一组没有公共顶点的边集合。最大匹配即包含边数最多的匹配。

增广路定理

Berge定理指出:一个匹配是最大匹配,当且仅当图中不存在增广路。增广路是指起点和终点均为未匹配顶点的交替路(匹配边与非匹配边交替出现)。沿着增广路翻转匹配状态,可使匹配大小增加1。

Hopcroft-Karp的优化策略

传统匈牙利算法每次只寻找一条增广路,时间复杂度为O(VE)。Hopcroft-Karp算法的核心改进在于:

  1. 分层BFS:从所有未匹配的U集合顶点同时出发,沿交替路进行BFS,构建距离层次图,找到最短的增广路集合
  2. 批量DFS增广:在分层图的基础上,从每个未匹配的U集合顶点出发进行DFS,只沿着BFS构建的距离层次前进,一次性找到多条不相交的最短增广路
  3. 交替执行:重复执行分层BFS和批量DFS增广,直到不存在增广路为止

由于每次找到的增广路都是当前最短的,且增广路长度严格递增,算法最多执行O(√V)轮即可收敛。

完整Java实现

import java.util.*;

/**
 * Hopcroft-Karp算法实现二分图最大匹配
 * 时间复杂度: O(E * sqrt(V))
 * 空间复杂度: O(V + E)
 */
public class HopcroftKarp {

    // 无穷大距离标记
    private static final int INF = Integer.MAX_VALUE;

    // 邻接表: U集合中每个顶点对应的V集合邻接顶点列表
    private final List<Integer>[] graph;

    // U集合顶点数
    private final int nU;

    // V集合顶点数
    private final int nV;

    // pairU[i] = U集合中顶点i匹配的V集合顶点编号,-1表示未匹配
    private final int[] pairU;

    // pairV[j] = V集合中顶点j匹配的U集合顶点编号,-1表示未匹配
    private final int[] pairV;

    // dist[i] = BFS分层图中U集合顶点i到起点的距离
    private final int[] dist;

    /**
     * 构造函数
     * @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.graph = new ArrayList[nU];
        for (int i = 0; i < nU; i++) {
            graph[i] = new ArrayList<>();
        }
        this.pairU = new int[nU];
        this.pairV = new int[nV];
        this.dist = new int[nU];
        Arrays.fill(pairU, -1);
        Arrays.fill(pairV, -1);
    }

    /**
     * 添加二分图边(从U集合指向V集合)
     * @param u U集合顶点编号 [0, nU)
     * @param v V集合顶点编号 [0, nV)
     */
    public void addEdge(int u, int v) {
        if (u < 0 || u >= nU || v < 0 || v >= nV) {
            throw new IllegalArgumentException("顶点编号越界");
        }
        graph[u].add(v);
    }

    /**
     * 执行Hopcroft-Karp算法,返回最大匹配数
     */
    public int maxMatching() {
        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:构建距离层次图,找到最短增广路的长度
     * 返回true表示存在增广路
     */
    private boolean bfs() {
        ArrayDeque<Integer> queue = new ArrayDeque<>();

        // 初始化:将所有未匹配的U集合顶点加入队列,距离设为0
        // 已匹配的U集合顶点距离设为INF(尚未到达)
        for (int u = 0; u < nU; u++) {
            if (pairU[u] == -1) {
                dist[u] = 0;
                queue.add(u);
            } else {
                dist[u] = INF;
            }
        }

        // 设置一个虚拟距离,表示是否到达过未匹配的V集合顶点
        // 如果BFS结束时没有到达任何未匹配的V顶点,则不存在增广路
        boolean foundAugmentingPath = false;

        while (!queue.isEmpty()) {
            int u = queue.poll();

            // 遍历u的所有邻接顶点v
            for (int v : graph[u]) {
                // 获取v的匹配对象u2(可能为-1)
                int u2 = pairV[v];

                if (u2 != -1 && dist[u2] == INF) {
                    // v已匹配给u2,通过匹配边到达u2
                    // u2的距离为u的距离+2(非匹配边u->v,匹配边v->u2)
                    dist[u2] = dist[u] + 2;
                    queue.add(u2);
                } else if (u2 == -1) {
                    // v未匹配,找到一条增广路的终点
                    foundAugmentingPath = true;
                }
            }
        }

        return foundAugmentingPath;
    }

    /**
     * DFS增广:在分层图上从u出发寻找增广路
     * 只沿着BFS构建的距离层次前进,保证找到的是最短增广路
     */
    private boolean dfs(int u) {
        for (int v : graph[u]) {
            int u2 = pairV[v];

            // 如果v未匹配,或者v的匹配对象u2可以通过分层图继续扩展
            // 注意:这里通过dist[u2] == dist[u] + 2来确保只走最短路径
            if (u2 == -1 || (dist[u2] == dist[u] + 2 && dfs(u2))) {
                // 更新匹配关系
                pairU[u] = v;
                pairV[v] = u;
                return true;
            }
        }

        // 从u出发无法找到增广路,标记dist[u]防止重复访问
        dist[u] = INF;
        return false;
    }

    /**
     * 获取匹配结果
     * @return U集合中每个顶点匹配的V集合顶点,-1表示未匹配
     */
    public int[] getPairU() {
        return pairU.clone();
    }

    /**
     * 打印匹配结果
     */
    public void printMatching() {
        System.out.println("最大匹配数: " + maxMatching());
        for (int u = 0; u < nU; u++) {
            if (pairU[u] != -1) {
                System.out.println("U" + u + " <-> V" + pairU[u]);
            }
        }
    }

    // ==================== 测试主程序 ====================

    public static void main(String[] args) {
        // 示例:员工-任务分配问题
        // U集合:4个员工 (0,1,2,3)
        // V集合:5个任务 (0,1,2,3,4)
        // 边表示员工可以执行的任务
        HopcroftKarp hk = new HopcroftKarp(4, 5);

        // 员工0可以执行任务0,1
        hk.addEdge(0, 0);
        hk.addEdge(0, 1);

        // 员工1可以执行任务1,2
        hk.addEdge(1, 1);
        hk.addEdge(1, 2);

        // 员工2可以执行任务2,3
        hk.addEdge(2, 2);
        hk.addEdge(2, 3);

        // 员工3可以执行任务0,3,4
        hk.addEdge(3, 0);
        hk.addEdge(3, 3);
        hk.addEdge(3, 4);

        int maxMatch = hk.maxMatching();
        System.out.println("最大匹配数: " + maxMatch); // 期望输出4

        System.out.println("匹配详情:");
        int[] matching = hk.getPairU();
        for (int u = 0; u < matching.length; u++) {
            if (matching[u] != -1) {
                System.out.println("  员工" + u + " -> 任务" + matching[u]);
            }
        }

        // 性能测试:大规模随机二分图
        System.out.println("\n=== 大规模性能测试 ===");
        int testNU = 5000;
        int testNV = 5000;
        int testEdges = 20000;

        HopcroftKarp largeHK = new HopcroftKarp(testNU, testNV);
        Random rand = new Random(42);
        for (int i = 0; i < testEdges; i++) {
            largeHK.addEdge(rand.nextInt(testNU), rand.nextInt(testNV));
        }

        long start = System.currentTimeMillis();
        int largeMatch = largeHK.maxMatching();
        long end = System.currentTimeMillis();

        System.out.println("顶点数: " + (testNU + testNV));
        System.out.println("边数: " + testEdges);
        System.out.println("最大匹配数: " + largeMatch);
        System.out.println("运行时间: " + (end - start) + "ms");
    }
}

算法正确性分析

Hopcroft-Karp算法的正确性依赖于以下关键性质:

性质1(最短增广路不相交):在同一轮BFS构建的分层图中,从各个未匹配U顶点出发的DFS找到的增广路是顶点不相交的。这是因为一旦某个顶点被用于增广路,其匹配状态即被更新,后续DFS不会再经过该顶点。

性质2(增广路长度单调递增):设第k轮找到的最短增广路长度为L_k,则L_{k+1} > L_k。这是因为第k轮已经找到了所有长度为L_k的增广路并进行了增广,剩余的增广路必然更长。

性质3(轮数上界):由于增广路长度最多为O(V),且每轮长度严格递增,而长度为l的增广路最多使匹配数增加O(V/l)条,因此总轮数为O(√V)。

复杂度分析

指标 复杂度 说明
时间复杂度 O(E√V) 每轮BFS+DFS为O(E),共O(√V)轮
空间复杂度 O(V+E) 邻接表、匹配数组、距离数组
与匈牙利算法对比 O(VE) vs O(E√V) 稠密图和大规模图优势显著

应用场景

  • 任务分配:将n个工人分配给m个任务,每个工人有技能限制
  • 网络流建模:二分图最大匹配可转化为最大流问题
  • 资源调度:CPU核心与进程、频谱与用户的匹配问题
  • 推荐系统:用户与商品的二分图匹配推荐

小结

Hopcroft-Karp算法通过分层BFS与批量DFS增广的协同策略,将二分图最大匹配的时间复杂度优化至O(E√V)。理解该算法的关键在于掌握增广路定理、最短增广路的不相交性质以及增广路长度的单调递增特性。在实际工程中,当二分图规模达到数千顶点、数万边时,Hopcroft-Karp相比朴素的匈牙利算法通常能提升数十倍性能。