Loading

「学习笔记」匈牙利算法

二分图匹配

定义:给定一个二分图 \(G\),在 \(G\) 的一个子图 \(M\) 中,\(M\) 的边集 \(E\) 中的任意两条边都不依附于同一个顶点,则称 \(M\) 是一个匹配。

匹配点:匹配边上的两点。

通俗一点就是一个二分图,每一个点最多只能选择连着一条边。


极大匹配

是指在当前已完成的匹配下,无法再增加未完成的匹配的边的方式来增加匹配的边数。

如图:

image
\(1 \rightarrow 4, 5 \rightarrow 8, 7 \rightarrow 6\) 是我们选择的边,这是一个极大匹配,

最大匹配

是指所有的极大匹配当中边数最大的一个匹配,设为 \(M\)。选择这样的边数最大的子集成为图的最大匹配问题。

最大匹配一定是极大匹配,极大匹配不一定是最大匹配。

如图:

image

\(1 \rightarrow 4\) 是我们选择的边,这是一个极大匹配,但是,它不是一个最大匹配。

完美匹配(完备匹配)

一个图中所有的顶点都是匹配点的匹配,即 \(M=2 \times V\)。完美匹配一定是最大匹配。

其实就是两部分的点一一对应。

如图:

image

这就是一个完美匹配。

最大匹配

最优匹配又称为带权最大匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大。一般 \(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;
}
posted @ 2022-08-09 20:35  yi_fan0305  阅读(295)  评论(0编辑  收藏  举报