定义
二分图
二分图是一种特殊的图,其顶点集合可以划分为两个互不相交的子集 和 ,使得图中的每条边都连接 中的顶点和 中的顶点。
匹配
匹配是指图中选取一组边,使得任意两条边都不共享顶点(即没有公共顶点)。
最大匹配
最大匹配是指包含边数最多的匹配。在二分图中,最大匹配的边数记为 。
完美匹配
如果 ,即所有顶点都被匹配,则称为完美匹配。
匈牙利算法(Hungarian Algorithm)
匈牙利算法由 Harold Kuhn 于 1955 年提出,用于求解二分图的最大匹配。
核心思想
不断寻找增广路径来增加匹配数。增广路径是指连接两个未匹配顶点的路径,其中未匹配的边和匹配的边交替出现。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m, e; // n: 左部点数, m: 右部点数, e: 边数
vector<int> g[MAXN]; // 邻接表
int matchR[MAXN]; // 右部点匹配的左部点
int d[MAXN]; // BFS 距离
bool visited[MAXN];
// 尝试从左部点 u 寻找增广路径
bool dfs(int u) {
for (int v : g[u]) {
if (!visited[v]) {
visited[v] = true;
if (matchR[v] == 0 || dfs(matchR[v])) {
matchR[v] = u;
return true;
}
}
}
return false;
}
// BFS 层次图构建(用于 Hopcroft-Karp)
bool bfs() {
queue<int> q;
for (int u = 1; u <= n; u++) {
if (matchL[u] == 0) { // 未匹配
d[u] = 0;
q.push(u);
} else {
d[u] = -1; // INF
}
}
bool found = false;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (matchR[v] && d[matchR[v]] == -1) {
d[matchR[v]] = d[u] + 1;
q.push(matchR[v]);
}
if (!matchR[v]) {
found = true; // 找到未匹配的右部点
}
}
}
return found;
}
int matchL[MAXN]; // 左部点匹配的右部点(可选,用于记录)
// Hopcroft-Karp 算法 - O(E sqrt(V))
int hopcroft_karp() {
int matching = 0;
while (bfs()) {
for (int u = 1; u <= n; u++) {
if (matchL[u] == 0) {
fill(visited, visited + m + 1, false);
if (dfs(u)) {
matching++;
}
}
}
}
return matching;
}
// 朴素匈牙利算法 - O(VE)
int hungarian() {
int ans = 0;
fill(matchR, matchR + m + 1, 0);
for (int u = 1; u <= n; u++) {
fill(visited, visited + m + 1, false);
if (dfs(u)) ans++;
}
return ans;
}朴素版本详解
// 朴素匈牙利算法(邻接矩阵版本)
int n, m; // n 左部顶点数,m 右部顶点数
int a[MAXN][MAXN]; // 邻接矩阵,a[u][v] = 1 表示有边
int match[MAXN]; // 右部点 v 匹配的左部点
bool vis[MAXN];
bool dfs(int u) {
for (int v = 1; v <= m; v++) {
if (a[u][v] && !vis[v]) {
vis[v] = true;
if (match[v] == 0 || dfs(match[v])) {
match[v] = u;
return true;
}
}
}
return false;
}
int hungarian() {
int ans = 0;
fill(match, match + m + 1, 0);
for (int u = 1; u <= n; u++) {
fill(vis, vis + m + 1, false);
if (dfs(u)) ans++;
}
return ans;
}KM 算法( Kuhn-Munkres Algorithm)
KM 算法用于求解二分图最大权匹配,即边权之和最大的匹配。
顶标思想
为左部点集合 中的每个顶点 维护顶标 ,为右部点集合 中的每个顶点 维护顶标 ,满足:
且相等子图( 的边构成的子图)中存在完美匹配。
代码实现
const int INF = 1e9;
int n; // 左部和右部顶点数相同
vector<vector<int>> w; // 边权矩阵 (1-indexed)
vector<int> lx, ly, match, slack;
vector<int> px, py; // 增广路记录
vector<bool> visx, visy;
bool dfs(int u) {
visx[u] = true;
for (int v = 1; v <= n; v++) {
if (visy[v]) continue;
int t = lx[u] + ly[v] - w[u][v];
if (t == 0) {
visy[v] = true;
if (match[v] == 0 || dfs(match[v])) {
match[v] = u;
return true;
}
} else {
slack[v] = min(slack[v], t);
}
}
return false;
}
int km() {
lx.assign(n + 1, 0);
ly.assign(n + 1, 0);
match.assign(n + 1, 0);
slack.assign(n + 1, INF);
// 初始化左部顶标为最大边权
for (int u = 1; u <= n; u++) {
lx[u] = *max_element(w[u].begin() + 1, w[u].end());
}
for (int u = 1; u <= n; u++) {
fill(slack.begin(), slack.end(), INF);
while (true) {
visx.assign(n + 1, false);
visy.assign(n + 1, false);
if (dfs(u)) break;
// 未能增广,需要修改顶标
int d = INF;
for (int v = 1; v <= n; v++) {
if (!visy[v]) d = min(d, slack[v]);
}
for (int i = 1; i <= n; i++) {
if (visx[i]) lx[i] -= d;
if (visy[i]) ly[i] += d;
else slack[i] -= d;
}
}
}
int ans = 0;
for (int v = 1; v <= n; v++) {
if (match[v]) ans += w[match[v]][v];
}
return ans;
}算法对比
| 算法 | 时间复杂度 | 适用场景 |
|---|---|---|
| 朴素匈牙利 | 小规模数据,最大匹配 | |
| Hopcroft-Karp | 大规模最大匹配 | |
| KM 算法 | 最大权匹配 |
应用场景
- 任务分配:将 n 个任务分配给 n 个工人,最小化总成本
- 运动员配对:双人比赛选手配对
- 婚礼座次:男女宾客配对
- 课程选择:学生与课程的匹配