题解:洛谷 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
【核心思想】
-
问题分析:给定一个二分图,左右各含 \(n\) 个节点,共有 \(m\) 条带权边,且保证存在完美匹配(即每个节点都能匹配到对侧的一个节点)。需要找出一组完美匹配,使得匹配边的权值之和最大。同时输出匹配方案。这是一个带权二分图最大匹配问题,也称最优匹配问题,常用 Kuhn-Munkres(KM)算法 求解。其核心是通过引入“顶标”(可行顶点标号)将原问题转化为在相等子图中寻找完美匹配,并通过不断调整顶标来扩大相等子图,直至找到最大权匹配。
-
算法选择:
- KM 算法(Kuhn-Munkres):专门求解二分图最大权完美匹配的经典算法。它基于“可行顶标”和“相等子图”的概念,通过逐步调整顶标,使得相等子图不断扩展,最终包含一个完美匹配,该匹配即为最大权匹配。
- DFS 实现(55分版本):递归搜索增广路,实现直观但效率较低,容易超时。
- BFS 实现(AC版本):使用 BFS 寻找增广路,并利用
slack数组和pre数组记录松弛量和前驱,单次增广复杂度 \(O(n^2)\),总复杂度 \(O(n^3)\),能通过本题数据(\(n \le 500\),虽然题目未明确,但典型数据范围)。 - 本模板提供两种实现,推荐 BFS 版,其效率更高且不易出现递归栈溢出。
-
关键步骤(以 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,将其加入交错树,重复上述过程,直到找到增广路。
- 初始化
- 若 \(i\) 未匹配,调用
- 计算答案:
ans = sum(w[matchR[j]][j]),其中matchR[j]是与右部点 \(j\) 匹配的左部点。 - 输出方案:输出
matchR[1..n]。
-
时间/空间复杂度:
- 时间复杂度:\(O(n^3)\),BFS 版 KM 每次增广 \(O(n^2)\),共 \(n\) 次增广。
- 空间复杂度:\(O(n^2)\),存储边权矩阵及辅助数组。
-
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
浙公网安备 33010602011771号