匈牙利算法用于解决 二分图最大匹配问题。
基本概念
-
匹配:找到一组边,每两条边之间没有公共点。
-
最大匹配:边数最多的匹配。
-
完美匹配:包含原图所有点的匹配。
-
匹配点 / 非匹配点:正在匹配当中的点(下图蓝色)/ 尚未匹配的点(下图绿色)
-
匹配边 / 非匹配边:正在匹配当中的边(下图蓝色)/ 尚未匹配的边(下图绿色)
-
增广路径:从一个非匹配点走到另一个非匹配点,途中经过交替经过非匹配边、匹配边、非匹配边、匹配边、…、非匹配边。(下图红色)
-
交错树:从未匹配点 r 开始寻找增广路时,交错路径组成的树。其中,称向根方向匹配的点为 S 点(黑点,偶点),背向根方向匹配的点为 T 点(白点,奇点)。
从匈牙利算法到带权带花树——详解对偶问题在图匹配上的应用
算法步骤
性质:
- 最大匹配 等价于 不存在增广路径
- 增广路径可以通过翻转整条路上边的选择状态获得一个长度恰好增加 1 的匹配。
因此只要不断寻找增广路径并进行翻转,就可以求得最大匹配(匹配不唯一)。
步骤:
- 枚举左部集每个点x
- 枚举x所连的右部集里的y
- 如果y没被匹配,则与x匹配,并记录y匹配的点match[y]=x,
- 如果y被匹配,则查看y所连的左部集的点match[y],看其是否能连接其他右部集的点(将match[y]当成x递归重复上述操作)。
- 只要能找到某个左部集的点能与其他未匹配的右部集点匹配,那么就找到了一条增广路,return true。
- 因为x是未匹配点,最后找到的未匹配的右部集点也是未匹配点,x所连的y被别人匹配了,这条边是未匹配边,y和match[y]所连的边是匹配边,因此这条路径是增广路径。
- 枚举x所连的右部集里的y
每个左部集的点匹配时,至多考虑1遍右部集的每个点,可用flag数组记录这次匹配右部集这个点是否被考虑过。
时间复杂度: O ( m n ) O(mn) O(mn)
算法实现
#include <iostream>
#include <cstring>
using namespace std;
const int N = 505, M = 50005;
int n1, n2, m; // n1:左部集合中的点数,n2:右部集合中的点数, m边数
int head[N], lnk[M], nxt[M], idx; // 只会用到从左部集合指向右部集合的边,所以这里只用存一个方向的边
bool flag[N]; // 判断右部集合是否考虑过,每个左部集的点匹配时,至多考虑1遍右部集的每个点
int match[N]; // 右部集合匹配的左部集合的结点编号
void add(int u, int v) {
lnk[idx] = v;
nxt[idx] = head[u];
head[u] = idx ++ ;
}
bool find(int x) {
for (int i = head[x]; i != -1; i = nxt[i]) {
int y = lnk[i];
if (!flag[y]) {
flag[y] = true;
if (!match[y] || find(match[y])) { // 如果y没被匹配,或者被匹配则考虑 match[y] 能否连其他右部集的点
match[y] = x;
return true;
}
}
}
return false;
}
int main() {
scanf("%d%d%d", &n1, &n2, &m);
memset(head, -1, sizeof head);
int u, v;
for (int i = 1; i <= m; i ++ ) {
scanf("%d%d", &u, &v);
add(u, v);
}
int res = 0;
for (int i = 1; i <= n1; i ++ ) { // 枚举左部集的每个点
memset(flag, false, sizeof flag); // 每次都要考虑1遍右部集所有点
if (find(i)) res ++ ;
}
printf("%d\n", res);
return 0;
}