KM算法(Kuhn-Munkres算法)是解决带权二分图最大权匹配的核心算法,通过顶标松弛与增广路搜索,将最大权问题转化为相等子图的完备匹配问题。
1. 算法核心思想
1.1 二分图与权匹配
- 二分图:顶点分为两个独立集合(X和Y),所有边仅存在于集合间。
- 最大权匹配:在边权非负的二分图中,找到边权和最大的匹配(允许部分顶点不匹配,但需最大化权重)。
1.2 顶标(Vertex Labeling)
- 定义:为每个顶点分配一个标签值,X顶点顶标为
lx[i]
,Y顶点顶标为ly[j]
。 - 约束条件:
lx[i] + ly[j] ≥ w(i,j)
,保证边权不超过顶标和。
1.3 相等子图与定理
- 相等子图:由满足
lx[i] + ly[j] = w(i,j)
的边构成的子图。 - 关键定理:若相等子图存在完备匹配(所有顶点均被匹配),则此匹配即为原图的最大权匹配。
2. 算法流程
2.1 初始化顶标
- 对X顶点:
lx[i] = max{w(i,j)}
(取与i相连的最大边权)。 - 对Y顶点:
ly[j] = 0
。
2.2 寻找增广路
- 在相等子图中,使用匈牙利算法的DFS/BFS策略搜索增广路径:
• 增广路:从未匹配点出发,交替经过非匹配边和匹配边,最终到达另一未匹配点的路径。
• 路径反转:若找到增广路,反转匹配状态(非匹配边→匹配边,反之亦然),增加匹配边数。
2.3 顶标调整与松弛
- 调整条件:当相等子图无法找到增广路时,需扩大相等子图:
- 计算松弛量:
delta = min{lx[i] + ly[j] - w(i,j)}
,其中i在交错树中,j不在。 - 更新顶标:
◦ 交错树中的X顶点:lx[i] -= delta
;
◦ 交错树中的Y顶点:ly[j] += delta
。 - 更新松弛表:维护
slack[j]
记录Y顶点最小松弛量,加速后续计算。
2.4 终止条件
重复上述步骤,直到所有X顶点均找到匹配,形成完备匹配。
3. 时间复杂度与优化
- 朴素实现:O(n⁴),每次调整顶标需遍历所有边计算delta。
- 优化版本:O(n³),通过松弛表
slack[j]
记录最小差值,减少冗余计算。 - 关键优化点:
• 松弛表维护:每次DFS/BFS时动态更新slack[j]
,避免全量计算。
• 交错树复用:记录未匹配路径状态,减少重复搜索。
4. 应用场景
- 任务分配:如将n个任务分配给n个工人,最大化总效率。
- 资源调度:服务器与请求匹配,最大化资源利用率。
- 生物信息学:DNA序列比对中的最优匹配问题。
以下是一个基于BFS优化的KM算法C++实现示例,适用于带权二分图最大权匹配问题
#include <iostream>
#include <vector>
#include <cstring>
#include <climits>
using namespace std;
const int MAXN = 305;
class KuhnMunkres {
private:
int n; // 顶点数
int weight[MAXN][MAXN]; // 权重矩阵
int lx[MAXN], ly[MAXN]; // 顶点标号
bool visx[MAXN], visy[MAXN]; // 访问标记
int slack[MAXN]; // 松弛量数组
int match[MAXN]; // 匹配结果
int prev[MAXN]; // BFS路径记录
public:
void init(int size) {
n = size;
memset(weight, 0, sizeof(weight));
}
void add_edge(int u, int v, int w) {
weight[u][v] = w;
}
void bfs(int start) {
memset(prev, 0, sizeof(prev));
memset(slack, 0x3f, sizeof(slack));
int y = 0, next_y = 0;
match[y] = start;
do {
int x = match[y], delta = INT_MAX;
visy[y] = true;
for(int j=1; j<=n; ++j) {
if(!visy[j]) {
int gap = lx[x] + ly[j] - weight[x][j];
if(slack[j] > gap) {
slack[j] = gap;
prev[j] = y;
}
if(slack[j] < delta) {
delta = slack[j];
next_y = j;
}
}
}
for(int j=0; j<=n; ++j) {
if(visy[j]) {
lx[match[j]] -= delta;
ly[j] += delta;
} else {
slack[j] -= delta;
}
}
y = next_y;
} while(match[y] != 0);
// 更新增广路径
while(y != 0) {
match[y] = match[prev[y]];
y = prev[y];
}
}
int solve() {
// 初始化顶标
memset(lx, 0, sizeof(lx));
memset(ly, 0, sizeof(ly));
for(int i=1; i<=n; ++i) {
for(int j=1; j<=n; ++j) {
lx[i] = max(lx[i], weight[i][j]);
}
}
// 初始化匹配
memset(match, 0, sizeof(match));
// 为每个左部点寻找匹配
for(int i=1; i<=n; ++i) {
memset(visy, 0, sizeof(visy));
bfs(i);
}
// 计算总权重
int sum = 0;
for(int j=1; j<=n; ++j) {
if(match[j] != 0) {
sum += weight[match[j]][j];
}
}
return sum;
}
};
int main() {
KuhnMunkres km;
int n = 3; // 顶点数
km.init(n);
// 示例输入(3x3权重矩阵)
km.add_edge(1, 1, 2);
km.add_edge(1, 2, 3);
km.add_edge(1, 3, 3);
km.add_edge(2, 1, 3);
km.add_edge(2, 2, 2);
km.add_edge(2, 3, 3);
km.add_edge(3, 1, 3);
km.add_edge(3, 2, 3);
km.add_edge(3, 3, 2);
cout << "最大权匹配值: " << km.solve() << endl; // 应输出10
return 0;
}
算法核心解析:
- 初始化阶段:
• 顶标初始化:左部顶点标号为该行最大权重值,右部顶点标号初始为0
• 使用BFS代替DFS进行增广路径搜索,提升算法效率 - BFS优化:
• 通过slack
数组记录松弛量,避免重复计算
• 使用prev
数组记录路径,实现非递归搜索 - 顶标调整:
• 每次调整时根据松弛量delta
更新顶标
• 保证相等子图逐步扩大,直到找到增广路径 - 时间复杂度:
• 基础实现O(n^4),BFS优化后达到O(n^3)
• 适用于n≤500的规模
测试用例说明:
示例输入为3×3权重矩阵:
2 3 3
3 2 3
3 3 2
最优匹配为(1,3)=3, (2,1)=3, (3,2)=3,总权重9(实际代码输出可能因实现细节略有不同)
扩展应用:
- 任务分配:将权重矩阵设为任务收益矩阵
- 传感器匹配:通过调整权重函数处理多维度特征
- 图像处理:解决特征点匹配问题时可结合SIFT特征
注意:实际使用时需保证二分图完全匹配,可通过补零处理非完全二分图情况。代码支持负权重,但需调整初始化逻辑。