二分图最大匹配是图论中的一个经典问题,在实际应用中有着广泛的用途。简单来说,二分图是指顶点可以划分为两个不相交的集合,使得每条边的两个端点分别属于这两个集合。而匹配则是指一组没有公共顶点的边集合。
匈牙利算法是解决二分图最大匹配问题最常用的方法之一,其时间复杂度为O(VE),其中V是顶点数,E是边数。这个算法由匈牙利数学家Dénes Kőnig在1931年提出,因此得名匈牙利算法。
在实际应用中,二分图匹配可以解决许多实际问题,比如:
匈牙利算法的核心思想是通过不断寻找增广路径来扩大当前的匹配。增广路径是指从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边...最后到达另一个未匹配点的路径。
算法的基本步骤如下:
在代码实现中,我们通常使用深度优先搜索(DFS)来寻找增广路径。以下是关键变量的作用:
adj[maxn]:邻接表,存储二分图的边pre[maxm]:记录右边顶点匹配的左边顶点visit[maxm]:标记右边顶点是否在当前DFS中被访问过算法的核心函数是Hungarian_findMatch,它尝试为左边的顶点u找到一个匹配。如果成功则返回true,否则返回false。
cpp复制#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
#define maxn 510
#define maxm 510
vector<int> adj[maxn]; // 邻接表存储
int pre[maxm]; // 匹配记录
bool visit[maxm]; // 访问标记
int n, m; // 左右顶点数
// 初始化函数
void Hungarian_Init(int n_, int m_) {
n = n_, m = m_;
memset(pre, -1, sizeof(pre));
for (int i = 1; i <= n; ++i) {
adj[i].clear();
}
}
// 添加边
void Hungarian_AddEdge(int u, int v) {
adj[u].push_back(v);
}
// 核心匹配函数
bool Hungarian_findMatch(int u) {
for (int i = 0; i < adj[u].size(); ++i) {
int v = adj[u][i];
if (!visit[v]) {
visit[v] = true;
int vpre = pre[v];
pre[v] = u;
if (vpre == -1 || Hungarian_findMatch(vpre)) {
return true;
}
pre[v] = vpre;
}
}
return false;
}
// 获取最大匹配数
int Hungarian_GetMaxMatch() {
int cnt = 0;
for (int i = 1; i <= n; ++i) {
memset(visit, false, sizeof(visit));
if (Hungarian_findMatch(i)) {
cnt++;
}
}
return cnt;
}
初始化部分:
Hungarian_Init函数负责初始化所有数据结构添加边:
Hungarian_AddEdge函数用于构建二分图核心匹配函数:
Hungarian_findMatch是算法的核心获取最大匹配数:
Hungarian_GetMaxMatch遍历所有左边顶点Hungarian_findMatch这是一个典型的二分图匹配应用场景:左边是求职者,右边是职位,边表示求职者可以胜任某个职位。
cpp复制int main() {
int n, m;
// n: 职位数量
// m: 求职者数量
cin >> n >> m;
Hungarian_Init(m, n);
for(int i=1; i <= m; ++i){
int k;
cin >> k;
while(k--){
int x;
cin >> x;
Hungarian_AddEdge(i, x);
}
}
cout << Hungarian_GetMaxMatch() << endl;
return 0;
}
在这个例子中:
这是一个更有趣的应用,将棋盘覆盖问题转化为二分图匹配问题。
cpp复制char c[25][25];
int dir[4][2] = {
{0, 1}, {1, 0}, {0, -1}, {-1, 0}
};
int main() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
cin >> c[i];
}
Hungarian_Init(n * n, n * n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if ((i + j) & 1) {
for (int k = 0; k < 4; ++k) {
int di = i + dir[k][0];
int dj = j + dir[k][1];
if (dj < 0 || di == n || dj < 0 || dj == n) {
continue;
}
if (c[i][j] == '1' || c[di][dj] == '1') {
continue;
}
Hungarian_AddEdge(i * n + j + 1, di * n + dj + 1);
}
}
}
}
cout << Hungarian_GetMaxMatch() << endl;
return 0;
}
这个问题的关键点:
邻接表优化:
访问标记优化:
提前终止:
顶点编号问题:
初始化问题:
边界条件处理:
调试技巧:
提示:在实际应用中,如果图的规模很大,可以考虑使用更高效的算法如Hopcroft-Karp算法,其时间复杂度为O(√VE)。
匈牙利算法可以扩展解决带权二分图匹配问题,即KM算法。这种情况下,每条边都有一个权重,目标是找到一个匹配使得所有匹配边的权重之和最大。
在某些情况下,一个顶点可能需要匹配多个顶点。这种问题可以通过将顶点拆分成多个副本转化为普通二分图匹配问题。
这是二分图匹配的一个著名变种,要求找到稳定的匹配方案,即不存在两对匹配中的成员都更倾向于对方而不是自己当前的匹配对象。
在实际使用匈牙利算法解决问题时,有几点经验值得分享:
问题建模是关键:很多实际问题需要巧妙地将问题转化为二分图匹配模型。这通常需要将问题中的实体分为两个不相交的集合,并确定它们之间的连接关系。
调试时从小开始:当算法出现问题时,先用小规模的测试用例进行调试,逐步验证每个步骤的正确性。
注意空间限制:虽然匈牙利算法时间复杂度尚可,但对于顶点数超过10000的图,可能需要考虑更高效的算法或优化手段。
预处理很重要:在实际应用中,对输入数据进行适当的预处理(如去除不可能匹配的边)可以显著提高算法效率。
理解算法的本质:不要仅仅记住代码模板,理解增广路径的概念和算法为什么有效,这样才能灵活应用到各种变种问题中。