云计算百科
云计算领域专业知识百科平台

KM 算法解析:带权二分图最优匹配与顶标调整

引言

在信奥新赛季的图论备考中,二分图匹配是一个绕不开的高频考点。前面我们聊过「二分图最大匹配」(匈牙利算法)求的是匹配边数最多;而真实竞赛里更常见的是——每对配对都有一个权重,要求选出的配对总权重最大。比如科技节上让同学和展位一一搭档,谁和哪个项目配合最默契,就是典型的「带权二分图最优匹配」问题。

解决它的算法叫 KM 算法(Kuhn–Munkres,也称匈牙利算法的带权版本),可以在 O(n³) 时间内求出最大权完美匹配。本文用一个原创实例,带你吃透顶标、相等子图、交错树与顶标调整这四块核心,并给出 C++ / Python 双版实现。

题目 / 项目目标

【原创题】校园科技节最佳搭档配对

有 n 名同学(左部集合 X)与 n 个展示项目(右部集合 Y),必须为每个同学分配且只分配一个项目,每个项目也恰好由一个同学负责。已知同学 i 对项目 j 的「适配默契度」为 w[i][j](可为负)。请设计一种一一配对方案,使所有配对的默契度之和最大。

以 n = 4 为例,默契度矩阵如下(行是同学,列是项目):

同学 \\ 项目项目0项目1项目2项目3
同学0 3 5 4 2
同学1 6 2 8 7
同学2 4 3 5 1
同学3 2 6 7 4

最优配对为:同学2→项目0(4)、同学0→项目1(5)、同学3→项目2(7)、同学1→项目3(7),总默契度 23。下面解释算法如何自动算出这个结果。

核心考点

  • 可行顶标(Feasible Labeling):给每个节点 i 一个顶标 l(i),要求对任意边 (u, v) 满足 l(u) + l(v) ≥ w(u, v)。初始令 lx[i] = max_j w[i][j]、ly[i] = 0 即是一组合法顶标。
  • 相等子图(Equality Subgraph):只保留满足 l(u) + l(v) == w(u, v) 的边构成的子图。定理:若相等子图存在完美匹配,则该匹配就是原图的最大权完美匹配(因为任意完美匹配权值和 ≤ 所有顶标之和)。
  • 增广路与交错树:在相等子图里用「匈牙利式」DFS 找增广路;找不到时,所有被访问到的点构成一棵交错树。
  • 顶标调整(松弛):找不到增广路时,计算最小可改进量 d = min(lx[x] + ly[y] – w[x][y])(x 在树内、y 在树外),让树内左部顶标 -d、树内右部顶标 +d,使至少一条新边进入相等子图,且不破坏已有匹配的可行性。
  • slack 数组:记录每个右部点「还需要减少多少」才能进入相等子图,避免每次重新扫描,把时间压到 O(n³)。
  • 补点技巧:若两边点数不等,把少的一边补虚拟点、虚边权设 0,转化为方阵求完美匹配;求最小权匹配只需把所有权取相反数。
  • 解法 / 拆解

    算法主流程为「为每个左部点依次找增广路」:初始顶标可行 → 在相等子图中 DFS 找增广 → 找到就匹配、处理下一个点;找不到就调整顶标,直到该点也能匹配。每个左部点最多触发 O(n) 次顶标调整,每次调整 O(n),DFS 本身 O(n),整体 O(n³)。

    C++ 实现

    #include <bits/stdc++.h>
    using namespace std;

    const int N = 310;
    const int INF = 1e9;
    int n;
    int w[N][N];
    int lx[N], ly[N];
    int match[N]; // match[y] = x(右部点 y 匹配到的左部点)
    bool visx[N], visy[N];
    int slack[N];

    // 为左部点 x 在相等子图中寻找增广路
    bool dfs(int x) {
    visx[x] = true;
    for (int y = 0; y < n; y++) {
    if (visy[y]) continue; // 右部点已在交错树中,跳过
    int gap = lx[x] + ly[y] – w[x][y]; // 与相等子图的距离
    if (gap == 0) { // 相等边:可走
    visy[y] = true;
    if (match[y] == -1 || dfs(match[y])) {
    match[y] = x;
    return true;
    }
    } else { // 记录松弛量
    slack[y] = min(slack[y], gap);
    }
    }
    return false;
    }

    int km() {
    memset(match, -1, sizeof(match));
    for (int i = 0; i < n; i++) {
    lx[i] = *max_element(w[i], w[i] + n); // 左部顶标取行最大值
    ly[i] = 0; // 右部顶标初始为 0
    }
    for (int cx = 0; cx < n; cx++) { // 依次为每个左部点找匹配
    fill(slack, slack + n, INF);
    while (true) {
    memset(visx, 0, sizeof(visx));
    memset(visy, 0, sizeof(visy));
    if (dfs(cx)) break; // 找到增广路,匹配成功
    int d = INF; // 最小可改进量
    for (int y = 0; y < n; y++)
    if (!visy[y]) d = min(d, slack[y]);
    // 调整顶标:交错树内左部 -d,右部 +d
    for (int x = 0; x < n; x++)
    if (visx[x]) lx[x] -= d;
    for (int y = 0; y < n; y++) {
    if (visy[y]) ly[y] += d;
    else slack[y] -= d;
    }
    }
    }
    int sum = 0;
    for (int y = 0; y < n; y++)
    if (match[y] != -1) sum += w[match[y]][y];
    return sum;
    }

    int main() {
    n = 4;
    int ex[4][4] = {
    {3,5,4,2},
    {6,2,8,7},
    {4,3,5,1},
    {2,6,7,4}
    };
    for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
    w[i][j] = ex[i][j];
    cout << km() << endl; // 输出 23
    return 0;
    }

    Python 实现

    def km_max(w, n):
    INF = float('inf')
    lx = [max(row) for row in w] # 左部顶标:每行最大值
    ly = [0] * n # 右部顶标
    match = [-1] * n # match[y] = x

    for cx in range(n): # 依次为每个左部点找增广路
    slack = [INF] * n
    while True:
    visx = [False] * n
    visy = [False] * n

    def dfs(x):
    visx[x] = True
    for y in range(n):
    if visy[y]:
    continue
    gap = lx[x] + ly[y] – w[x][y]
    if gap == 0: # 相等边
    visy[y] = True
    if match[y] == -1 or dfs(match[y]):
    match[y] = x
    return True
    else:
    slack[y] = min(slack[y], gap)
    return False

    if dfs(cx):
    break
    d = min(slack[y] for y in range(n) if not visy[y])
    for x in range(n):
    if visx[x]:
    lx[x] -= d
    for y in range(n):
    if visy[y]:
    ly[y] += d
    else:
    slack[y] -= d

    return sum(w[match[y]][y] for y in range(n))

    # 示例
    w = [
    [3, 5, 4, 2],
    [6, 2, 8, 7],
    [4, 3, 5, 1],
    [2, 6, 7, 4],
    ]
    print(km_max(w, 4)) # 输出 23

    跑上面的例子,两种语言都输出 23,与手算的最优方案一致:同学2→项目0、同学0→项目1、同学3→项目2、同学1→项目3。

    时间 / 空间复杂度

    • 时间复杂度:O(n³)。外层为 n 个左部点,每个点最多 O(n) 次顶标调整,每次调整与 DFS 均为 O(n)。
    • 空间复杂度:O(n²),主要来自权重矩阵 w[n][n] 与若干长度为 n 的辅助数组(顶标、标记、slack)。

    易错点

  • 顶标初始化错误:必须 lx[i] = max_j w[i][j],而不是简单取 0 或随意赋值,否则初始顶标可能不可行(存在 lx+ly < w 的边)。
  • 相等边判断用 == 而非 ≤:只走 lx+ly == w 的「相等边」,走 ≥ 的边会破坏相等子图定理,导致结果不是最大权。
  • 顶标调整只动交错树内的点:lx[x] -= d 仅对 visx[x] 为真者、ly[y] += d 仅对 visy[y] 为真者;树外点动不得,否则会破坏已有的可行顶标性质。
  • d 必须取最小:d = min(slack[y])(y 在树外)。取大会跳过本应进入的边;取小虽仍正确但效率低。
  • slack 的传递更新:每次顶标 -d 后,树外点的 slack 要 -d(因为 gap 同减 d)。若每次重新全量计算虽正确但退化为 O(n⁴),失去优化意义。
  • 非方阵要补点:左右点数不等时先把少的一侧补虚拟点、虚边权设 0,否则「完美匹配」前提不成立,算法会漏解。
  • 进阶

    • 最小权匹配:把所有边权取相反数跑最大权 KM,结果再取反即可。
    • 非方阵 / 最大权匹配(不一定完备):把缺失的边权补 0 后求完美匹配;若只求「边数最多且权最大」,可补足够小的负权虚拟边。
    • 与费用流的关系:带权二分图最大权匹配等价于二分图最小费用最大流(边权取负),KM 是专门针对「完美匹配」这一特殊结构的 O(n³) 高效算法;规模很大或带容量限制时改用费用流更通用。
    • 匈牙利算法是 KM 的特例:把所有边权都视为 1,KM 退化为普通二分图最大匹配。
    • 经典模板题:洛谷 P6577【模板】二分图最大权完美匹配,可拿来练手验证自己的板子。

    小结与互动

    KM 算法的灵魂在于「用顶标把难解的最大权匹配,缩到相等子图里用熟悉的增广路去解」。记住三步曲:顶标初始化 → 相等子图找增广 → 找不到就调顶标,再配合 slack 把复杂度压到 O(n³),这道题就稳了。

    👉 你第一次学 KM 时,最容易卡在哪一步?是顶标调整的方向,还是相等子图的定理证明?欢迎在评论区聊聊,也欢迎把这篇分享给正在备赛的同学~


    📚 免费少儿编程资料(夸克网盘领取)

    以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  • 全国青少年信息素养大赛复赛集训题目Python&C++.docx https://pan.quark.cn/s/93995d3cb150
  • 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf https://pan.quark.cn/s/da97b5dbf75d
  • Python背记手册.pdf https://pan.quark.cn/s/7568ae9ca92b
  • Python课程 https://pan.quark.cn/s/a94bf02d00c6
  • 2024信息素养大赛图形化复赛集训题答案3-9 https://pan.quark.cn/s/6ccab7ec3cbc
  • 2025年03月份电子学会考级真题 https://pan.quark.cn/s/4403c4228912
  • 2025全国青少年信息素养大赛赛项说明 https://pan.quark.cn/s/d9d0df4a9f29
  • 青少儿信息素养大赛编程资料 https://pan.quark.cn/s/4ab6bd83be8a
  • 资料持续更新,关注获取最新分享。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » KM 算法解析:带权二分图最优匹配与顶标调整
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!