「学习笔记」匈牙利算法
二分图匹配
定义:给定一个二分图 \(G\),在 \(G\) 的一个子图 \(M\) 中,\(M\) 的边集 \(E\) 中的任意两条边都不依附于同一个顶点,则称 \(M\) 是一个匹配。
匹配点:匹配边上的两点。
通俗一点就是一个二分图,每一个点最多只能选择连着一条边。
极大匹配
是指在当前已完成的匹配下,无法再增加未完成的匹配的边的方式来增加匹配的边数。
如图:
\(1 \rightarrow 4, 5 \rightarrow 8, 7 \rightarrow 6\) 是我们选择的边,这是一个极大匹配,
最大匹配
是指所有的极大匹配当中边数最大的一个匹配,设为 \(M\)。选择这样的边数最大的子集成为图的最大匹配问题。
最大匹配一定是极大匹配,极大匹配不一定是最大匹配。
如图:
\(1 \rightarrow 4\) 是我们选择的边,这是一个极大匹配,但是,它不是一个最大匹配。
完美匹配(完备匹配)
一个图中所有的顶点都是匹配点的匹配,即 \(M=2 \times V\)。完美匹配一定是最大匹配。
其实就是两部分的点一一对应。
如图:
这就是一个完美匹配。
最大匹配
最优匹配又称为带权最大匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大。一般 \(X\) 和 \(Y\) 集合顶点个数相同,最优匹配也是一个完美匹配,即每一个顶点都被匹配。如果个数不相等,可以通过补点加 \(0\) 边实现转化。一般会使用 KM 算法来解决此问题,这个我们后面再说。
二分图最大匹配
先来看几个定义:
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边,非匹配边......形成的路径叫做交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发点不算),则这条交替路称之为增广路。
增广路有一个重要特点:非匹配边比匹配边多一条。因此研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份互换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配数目比原来多了一条。
我们可以通过不停的找增广路来增加匹配中的匹配边和匹配点。找不到增广路时就是最大匹配了(增广路定理)。
匈牙利算法
其实匈牙利算法就是类似于生活中你和一些人选东西,以座位为例,你想选这个座位,如果别人没选,那你就直接坐下,如果已经有别人了,你要先和这个人协商,看看人家愿不愿意让给你,让,你就坐;否则,你就要找其他的位置 要不你站着。
dfs 版本
数组、存图等的准备
#include <iostream>
#include <cstdio>
#include <cstring>
typedef long long ll;
using namespace std;
const int N = 510;
const int M = 5e4 + 5;
int n, m, ed, cnt, tim, ans;
int h[N], vis[N], ask[N];// vis 本次匹配是否访问过 ask 该节点的匹配,为0,则还没有匹配
struct edge {// 链式前向星
int v, nxt;
} e[M << 1];
inline ll read() {// 快读
ll x = 0;
int fg = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
void add(int u, int v) {// 加边,建图
e[++cnt].v = v;
e[cnt].nxt = h[u];
h[u] = cnt;
}
核心代码——dfs:
inline int dfs(int u) {
int v;
for(int i = h[u]; i; i = e[i].nxt) {
v = e[i].v;
if(vis[v]) continue;//这个点能不能选或者能不能“抢”
vis[v] = 1;//能选 或 能“抢”但要协商
if(!ask[v] || dfs(ask[v])) {
//ask[v] == 0不用协商就能选
//否则就dfs(ask[v]),看看原主人能不能换别人(协商)
ask[v] = u;//直接选上 或 协商成功
return 1;//匹配成功
}
}
return 0;//没有连边或协商失败——匹配失败
}
ll match() {
for(int i = 1; i <= n; ++i) {
memset(vis, 0, sizeof vis);//为了满足i的要求,将数组清0,让他可以任意选
ans += dfs(i);//匹配成功+1,否则+0
}
return ans;
}
注释我觉得挺详细的了,dfs 版本比较好理解,也比较好些写,但是许多博主和 dalao 都说 bfs 版本要比 dfs 快一点,所以,我又去学习并理解的 bfs 匈牙利算法,
bfs 版本
bfs 相对于 dfs 的过程差不多,只是 dfs 是通过递归来到这个点来让它找新点,而 bfs 是放进队列中了而已
必要的准备
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
typedef long long ll;
using namespace std;
const int N = 510;
const int M = 5e4 + 5;
int n, m, edge_num, cnt, ans;
int h[N], px[N], py[N];// px 左边的点的匹配点 py 右边的点的匹配点
int vis[N], pre[N];// vis 本轮是否访问过 pre 右边点的前驱
struct edge {// 链式前向星
int v, nxt;
} e[M << 1];
inline ll read() {// 快读
ll x = 0;
int fg = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
void add(int u, int v) {// 加边
e[++cnt].v = v;
e[cnt].nxt = h[u];
h[u] = cnt;
}
老实说,bfs 版本是真的难理解,代码也不好写,在数据比较小的情况下还是用 dfs 更好一些,一定要先理解 dfs 版本再来看 bfs 版本!!!
首先,还是匈牙利的前置函数,没什么可说的
ll match() {
for(int i = 1; i <= n; ++i) {
ans += bfs(i);
}
return ans;
}
然后就是 bfs 的过程
ll bfs(int x) {
memset(vis, 0, sizeof vis);//其实这里也可以记录时间戳来判断是否在本轮被标记
memset(pre, 0, sizeof pre);//前驱一定要清0
queue<int> q;//队列定义在函数内相较于定义在函数外可以免去清空的过程
q.push(x);// 先将本轮要匹配的点入队
while(!q.empty()) {
int u = q.front();// 取队首
q.pop();// 弹出
for(int i = h[u]; i; i = e[i].nxt) {
int v = e[i].v;
if(vis[v]) continue;// 如果本轮他已经被选过了,跳过
vis[v] = 1;// 标记
pre[v] = u;// 记录v点是被谁搜到的,即记录前驱
if(!py[v]) {// 如果v点还没有匹配
aug(v);// 进行匹配
return 1;
}
else {
q.push(py[v]);// 否则就默认点v与当前点u匹配了,原主人入队去找新点
}
}
}
return 0;
}
其实队列的作用与原 bfs 的队列一样,谁要找点谁就入队,找到一个点,没被匹配就直接匹配,匹配了就与原主人协商
接下来是 aug 匹配函数
void aug(int x) {
while(x) {
int temp = px[pre[x]];//记录搜到当前点的点原来的匹配temp
px[pre[x]] = x;
py[x] = pre[x];//将两点进行匹配
x = temp;//将temp带入循环,让他去匹配
}
}
来解释一下,首先,能进入这个函数,一定是右边的点没有被匹配,但是,我们不能确定左边的点是否已经被匹配。
当这个点 \(a\) 是被迫换新点时,它与它原来的配对点 \(b\) 配对关系依旧存在,直到 \(a\) 与其他点配对成功了,原关系才破裂(这里的情况是被迫换新点,所以该点 \(a\) 的原匹配点 \(b\) 就与强迫 \(a\) 换的点的点 \(c\) 进行匹配 点c 是与点 a 抢人的)
更新
dfs 时间戳优化匈牙利算法
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 510;
int n, m, e, tim;
vector<int> son[N];
int ask[N], vis[N];
int dfs(int u) {
for (int v : son[u]) {
if (vis[v] == tim) continue;
vis[v] = tim;
if (!ask[v] || dfs(ask[v])) {
ask[v] = u;
return 1;
}
}
return 0;
}
int main() {
scanf("%d%d%d", &n, &m, &e);
for (int i = 1, u, v; i <= e; ++ i) {
scanf("%d%d", &u, &v);
son[u].push_back(v);
}
int ans = 0;
for (int i = 1; i <= n; ++ i) {
++ tim;
ans += dfs(i);
}
printf("%d\n", ans);
return 0;
}