题解:洛谷 P6577 二分图最大完美匹配

【题目来源】

洛谷:P6577 【模板】二分图最大权完美匹配 - 洛谷

【题目描述】

给定一张二分图,左右部均有 \(n\) 个点,共有 \(m\) 条带权边,且保证有完美匹配。

求一种完美匹配的方案,使得最终匹配边的边权之和最大。

【输入】

第一行两个整数 \(n,m\),含义见题目描述。

\(2\sim m+1\) 行,每行三个整数 \(y,c,h\) 描述了图中的一条从左部的 \(y\) 号结点到右部的 \(c\) 号节点,边权为 \(h\) 的边。

【输出】

本题存在 Special Judge

第一行一个整数 \(ans\) 表示答案。

第二行共 \(n\) 个整数 \(a_1,a_2,a_3\cdots a_n\),其中 \(a_i\) 表示完美匹配下与右部\(i\) 个点相匹配的左部点的编号。如果存在多种方案,请输出任意一种。

【输入样例】

5 7
5 1 19980600
4 2 19980587
1 3 19980635
3 4 19980559
2 5 19980626
1 2 -15484297
4 5 -17558732

【输出样例】

99903007
5 4 1 3 2

【核心思想】

  1. 问题分析:给定一个二分图,左右各含 \(n\) 个节点,共有 \(m\) 条带权边,且保证存在完美匹配(即每个节点都能匹配到对侧的一个节点)。需要找出一组完美匹配,使得匹配边的权值之和最大。同时输出匹配方案。这是一个带权二分图最大匹配问题,也称最优匹配问题,常用 Kuhn-Munkres(KM)算法 求解。其核心是通过引入“顶标”(可行顶点标号)将原问题转化为在相等子图中寻找完美匹配,并通过不断调整顶标来扩大相等子图,直至找到最大权匹配。

  2. 算法选择

    • KM 算法(Kuhn-Munkres):专门求解二分图最大权完美匹配的经典算法。它基于“可行顶标”和“相等子图”的概念,通过逐步调整顶标,使得相等子图不断扩展,最终包含一个完美匹配,该匹配即为最大权匹配。
    • DFS 实现(55分版本):递归搜索增广路,实现直观但效率较低,容易超时。
    • BFS 实现(AC版本):使用 BFS 寻找增广路,并利用 slack 数组和 pre 数组记录松弛量和前驱,单次增广复杂度 \(O(n^2)\),总复杂度 \(O(n^3)\),能通过本题数据(\(n \le 500\),虽然题目未明确,但典型数据范围)。
    • 本模板提供两种实现,推荐 BFS 版,其效率更高且不易出现递归栈溢出。
  3. 关键步骤(以 BFS 版 KM 为例):

    • 建图:读入 \(n, m\),将左部点 \(i\) 到右部点 \(j\) 的边权存入 \(w[i][j]\),不存在的边权设为负无穷(或极小值)。
    • 初始化顶标:左部顶标 \(la[i] = \max_j w[i][j]\),右部顶标 \(lb[j] = 0\)。初始相等子图由满足 \(la[i] + lb[j] = w[i][j]\) 的边构成。
    • 为每个左部点寻找增广路(循环 \(i = 1..n\)):
      • \(i\) 未匹配,调用 bfs(i) 寻找增广路。
      • bfs 流程:
        • 初始化 va, vb, slack, pre,将左部点 \(i\) 入队并标记 va[i]=1
        • BFS 扩展:从队列取出左部点 \(x\),遍历所有右部点 \(y\),若 \(y\) 不在交错树中:
          • 计算松弛量 gap = la[x] + lb[y] - w[x][y]
          • gap < slack[y],更新 slack[y] = gap, pre[y] = x
          • gap == 0,则边 \(x-y\) 在相等子图中,将 \(y\) 加入交错树(vb[y]=1)。若 \(y\) 未匹配,则找到增广路,调用 augment(y) 回溯更新匹配;否则将 \(y\) 的匹配点加入队列继续 BFS。
        • 若 BFS 未找到增广路,则计算最小松弛量 delta = min(slack[y])\(y\) 不在交错树中),调整顶标:所有在交错树中的左部点 la -= delta,所有在交错树中的右部点 lb += delta;同时更新所有未访问右部点的 slack[y] -= delta
        • 检查是否有新的右部点满足 slack[y] == 0,将其加入交错树,重复上述过程,直到找到增广路。
    • 计算答案ans = sum(w[matchR[j]][j]),其中 matchR[j] 是与右部点 \(j\) 匹配的左部点。
    • 输出方案:输出 matchR[1..n]
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^3)\),BFS 版 KM 每次增广 \(O(n^2)\),共 \(n\) 次增广。
    • 空间复杂度:\(O(n^2)\),存储边权矩阵及辅助数组。
  5. KM 算法的核心思想

    • 可行顶标:一组满足 \(la[i] + lb[j] \ge w[i][j]\) 的实数,保证匹配权值上界为 \(\sum la + \sum lb\)
    • 相等子图:由满足 \(la[i] + lb[j] = w[i][j]\) 的边构成。若相等子图存在完美匹配,则该匹配即为最大权匹配。
    • 顶标调整:若当前相等子图无完美匹配,则通过 BFS 找到最小的松弛量 \(\delta\),调整交错树内顶标,使至少一条新边进入相等子图,而不会破坏可行性。
    • 增广路与匹配更新:在相等子图中寻找增广路,若找到则翻转匹配,否则调整顶标后继续。重复此过程直至所有左部点匹配。
    • 适用场景:任务分配、最大权二分图匹配、求最大权完美匹配问题。

【算法标签】

省选 #二分图

【代码详解】

// 55分
#include <bits/stdc++.h>
using namespace std;
#define int long long           // 将 int 定义为 long long,防止数值溢出
const int N = 505 * 2;          // 最大节点数(左右部各n个点,总2n个)
int n, m;                       // n: 每部点数, m: 边数
int y, c, h;                    // 临时变量(未使用)
int match[N];                   // match[y]: 右部点y匹配的左部点编号
int va[N], vb[N];               // va[x]: 左部点x是否在交错树中, vb[y]: 右部点y是否在交错树中
int la[N], lb[N];               // la[x]: 左部点x的顶标, lb[y]: 右部点y的顶标
int w[N][N];                    // w[x][y]: 左部点x到右部点y的边权
int d[N];                       // d[y]: 右部点y的松弛量(到交错树的最小距离)

// DFS增广:尝试为左部点x找到匹配
// 在相等子图中寻找增广路
bool dfs(int x)
{
    va[x] = 1;                  // 标记x在交错树中

    // 枚举所有右部点y
    for (int y = 1; y <= n; y++)
    {
        if (!vb[y])             // y不在交错树中
        {
            // 检查x-y是否在相等子图中(顶标和等于边权)
            if (la[x] + lb[y] - w[x][y] == 0)
            {
                vb[y] = 1;      // 将y加入交错树

                // y未匹配,或y的匹配点可以找到新的匹配(递归增广)
                if (!match[y] || dfs(match[y]))
                {
                    match[y] = x;   // x匹配y
                    return 1;       // 增广成功
                }
            }
            else
            {
                // 不在相等子图中,更新松弛量
                // d[y]记录y到交错树的最小距离
                d[y] = min(d[y], la[x] + lb[y] - w[x][y]);
            }
        }
    }
    return 0;                   // 增广失败
}

// KM算法(Kuhn-Munkres):求二分图最大权完美匹配
int KM()
{
    // 初始化左部顶标:取每个左部点连出的最大边权
    for (int i = 1; i <= n; i++)
        la[i] = -1e18;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            la[i] = max(la[i], w[i][j]);

    // 初始化右部顶标为0
    for (int i = 1; i <= n; i++)
        lb[i] = 0;

    // 依次为每个左部点寻找匹配
    for (int i = 1; i <= n; i++)
    {
        while (true)
        {
            // 每次尝试增广前,清空标记和松弛量
            fill(va + 1, va + n + 1, 0);
            fill(vb + 1, vb + n + 1, 0);
            fill(d + 1, d + n + 1, 1e18);

            // 尝试在相等子图中为左部点i找到增广路
            if (dfs(i))
                break;          // 增广成功,退出循环

            // 增广失败,需要调整顶标,扩大相等子图
            int delta = 1e18;
            // 找到不在交错树中的右部点的最小松弛量
            for (int j = 1; j <= n; j++)
                if (!vb[j])
                    delta = min(delta, d[j]);

            // 调整顶标:交错树中的左部点顶标减delta,右部点顶标加delta
            for (int j = 1; j <= n; j++)
            {
                if (va[j])
                    la[j] -= delta;     // 左部点在交错树中,顶标减小
                if (vb[j])
                    lb[j] += delta;     // 右部点在交错树中,顶标增大
            }
            // 调整后,至少有一条新边进入相等子图,继续尝试增广
        }
    }

    // 计算最大权匹配的总权值
    int res = 0;
    for (int i = 1; i <= n; i++)
        res += w[match[i]][i];  // match[i]是与右部点i匹配的左部点

    return res;
}

signed main()                   // 使用 signed 替代 int,因为 #define int long long
{
    cin >> n >> m;              // 读入每部点数和边数

    // 初始化边权为负无穷(表示无边)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            w[i][j] = -1e18;

    // 读入m条边
    while (m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        w[a][b] = c;            // 左部点a到右部点b的边权为c
    }

    // 输出最大权完美匹配的总权值
    cout << KM() << endl;

    // 输出匹配方案:右部点i匹配的是左部点match[i]
    for (int i = 1; i <= n; i++)
        cout << match[i] << " ";
    cout << endl;

    return 0;
}
// AC版本
#include <bits/stdc++.h>
using namespace std;
#define int long long           // 将 int 定义为 long long,防止数值溢出
const int N = 505 * 2;          // 最大节点数(左右部各n个点,总2n个)
int n, m;                       // n: 每部点数, m: 边数
int w[N][N];                    // w[x][y]: 左部点x到右部点y的边权
int la[N], lb[N];               // la[x]: 左部点x的顶标, lb[y]: 右部点y的顶标
int va[N], vb[N];               // va[x]: 左部点x是否在交错树中, vb[y]: 右部点y是否在交错树中
int matchR[N];                  // matchR[y]: 与右部点y匹配的左部点编号
int matchL[N];                  // matchL[x]: 与左部点x匹配的右部点编号
int pre[N];                     // pre[y]: 在BFS中,右部点y的前驱左部点(用于回溯增广路)
int slack[N];                   // slack[y]: 右部点y到交错树的最小松弛量

// 增广操作:从右部点y出发,沿pre数组回溯,翻转匹配边
void augment(int y)
{
    while (y)
    {
        int x = pre[y];         // y的前驱左部点
        int nxt = matchL[x];    // x原来匹配的右部点(可能为0,表示x未匹配)

        // 将x和y匹配
        matchR[y] = x;
        matchL[x] = y;

        y = nxt;                // 继续处理x原来匹配的右部点
    }
}

// BFS寻找从左部点u出发的增广路(在相等子图中)
// 若找不到,则调整顶标扩大相等子图,直到找到增广路
bool bfs(int u)
{
    queue<int> q;
    q.push(u);

    // 每次BFS前清空标记和松弛量
    fill(va + 1, va + n + 1, 0);
    fill(vb + 1, vb + n + 1, 0);
    fill(slack + 1, slack + n + 1, (int)1e18);

    va[u] = 1;                  // 标记起点u在交错树中

    while (true)
    {
        // 第一阶段:在相等子图中BFS
        while (!q.empty())
        {
            int x = q.front(); q.pop();

            // 枚举所有右部点y
            for (int y = 1; y <= n; y++)
            {
                if (vb[y]) continue;    // y已在交错树中,跳过

                // 计算x到y的松弛量:顶标和减去边权
                int gap = la[x] + lb[y] - w[x][y];

                if (gap < slack[y])     // 找到更小的松弛量
                {
                    slack[y] = gap;     // 更新y的松弛量
                    pre[y] = x;         // 记录y的前驱为x

                    if (gap == 0)       // x-y在相等子图中
                    {
                        vb[y] = 1;      // 将y加入交错树

                        if (!matchR[y]) // y未匹配,找到增广路
                        {
                            augment(y); // 沿pre回溯增广
                            return true;
                        }

                        // y已匹配,将其匹配点加入队列继续BFS
                        q.push(matchR[y]);
                        va[matchR[y]] = 1;
                    }
                }
            }
        }

        // 第二阶段:增广失败,计算最小松弛量delta,调整顶标
        int delta = (int)1e18;
        for (int y = 1; y <= n; y++)
            if (!vb[y])
                delta = min(delta, slack[y]);

        // 调整顶标:交错树中的左部点顶标减delta,右部点顶标加delta
        for (int i = 1; i <= n; i++)
        {
            if (va[i])
                la[i] -= delta;
            if (vb[i])
                lb[i] += delta;
        }

        // 第三阶段:检查是否有新的边进入相等子图,继续BFS
        for (int y = 1; y <= n; y++)
        {
            if (!vb[y] && slack[y] == delta)    // y的松弛量恰好等于delta,进入相等子图
            {
                vb[y] = 1;                      // 将y加入交错树

                if (!matchR[y])                 // y未匹配,找到增广路
                {
                    augment(y);                 // 沿pre回溯增广
                    return true;
                }

                // y已匹配,将其匹配点加入队列继续BFS
                q.push(matchR[y]);
                va[matchR[y]] = 1;
            }
            slack[y] -= delta;                  // 所有未访问的右部点松弛量减delta
        }
    }
    return false;
}

// KM算法(Kuhn-Munkres):求二分图最大权完美匹配
// 本实现采用BFS版本,效率优于DFS版本
int KM()
{
    // 初始化左部顶标:取每个左部点连出的最大边权
    for (int i = 1; i <= n; i++)
    {
        la[i] = -1e18;
        for (int j = 1; j <= n; j++)
            la[i] = max(la[i], w[i][j]);
    }

    // 初始化右部顶标为0
    for (int i = 1; i <= n; i++)
        lb[i] = 0;

    // 依次为每个未匹配的左部点寻找增广路
    for (int i = 1; i <= n; i++)
    {
        if (!matchL[i])             // 左部点i未匹配
            bfs(i);                 // 从i出发BFS找增广路
    }

    // 计算最大权匹配的总权值
    int res = 0;
    for (int i = 1; i <= n; i++)
        if (matchR[i])
            res += w[matchR[i]][i]; // matchR[i]是与右部点i匹配的左部点

    return res;
}

signed main()                       // 使用 signed 替代 int,因为 #define int long long
{
    cin >> n >> m;                  // 读入每部点数和边数

    // 初始化边权为负无穷(表示无边)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            w[i][j] = -1e18;

    // 读入m条边
    while (m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        w[a][b] = c;                // 左部点a到右部点b的边权为c
    }

    // 输出最大权完美匹配的总权值
    cout << KM() << endl;

    // 输出匹配方案:右部点i匹配的是左部点matchR[i]
    for (int i = 1; i <= n; i++)
        cout << matchR[i] << " ";
    cout << endl;

    return 0;
}

【运行结果】

5 7
5 1 19980600
4 2 19980587
1 3 19980635
3 4 19980559
2 5 19980626
1 2 -15484297
4 5 -17558732
99903007
5 4 1 3 2
posted @ 2026-09-01 22:40  团爸讲算法  阅读(19)  评论(0)    收藏  举报