二分图匹配——匈牙利算法
我是在下面这篇博客里自学的,里面把相关概念和算法都讲得十分清楚。
实在看不懂再回来看我的低配版吧。
Renfei Song’s Blog
预备知识
引用自👆博客
二分图:简单来说,如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。
匹配:在图论中,一个「匹配」(matching)是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
匹配边即连接两个匹配点所用的边。
没有被用来匹配的边就是非匹配边。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
个人理解是一定要从非匹配边开始,非匹配边结束,因为其中非匹配边和匹配边交替出现。
所以总边数必然是奇数,且非匹配边比匹配边多一条。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)
算法理解
化为寻找增广路径总数
将二分图分为左右两个集合。
由于增广路的特性——先走未匹配边,后走匹配边,依次轮换。所以每次从左边的集合中选一个点找增广路,终点必然在右边的集合中,并且非匹配边比匹配边数多一条。
核心:找到增广路后,只需要将增广路径上的边反转(匹配边和非匹配边反转)即可让匹配数加一。
至此,二分图的最大匹配问题可以完全转化为寻找增广路总数的问题。
寻找增广路径总数呢
实际上bfs和dfs均可实现 ,目前只学了代码量较少的dfs方法。
由增广路特性可知,从左集合走到右边集合必然使用一条非匹配边。从右集合走到左集合必然使用一条匹配边。
所以用一个数组记录右集合点i的匹配左集合点j match[i] = j;
为了防止死循环所以用集合vis[]来标记被匹配过的右集合点。
增广路从一个新的(只要正常枚举左端点就能保证当前端点都没有被匹配过)左端点开始。
当前点在左集合时,枚举能走到的右集合点看是否能匹配。若能则直接匹配上,结束。若右集合的点已经被匹配则延长交替路,通过match数组找回左集合点。一直循环直到找到一个未匹配的右端点时结束。
代码实现
vector<int>vec[maxn];//用于存放左集合点能连接到的点。
bool vis[maxn];//用于防止递归死循环。
int match[maxn];//用于记录右集合的匹配点。
bool dfs(int x)
{
for (int i : vec[x]) { // 当前点的每个邻接点
if (!vis[i]) { // 要求不在当前交替路中
//要注意的是vis数组会在枚举左集合点的时候重置为false
vis[i] = true; // 放入交替路
if (match[i] == 0 || dfs(match[i])) {
// 如果右集合的点未匹配,说明交替路为增广路,则记录匹配,并返回成功
match[i] = x;
return true;
}
}
}
return false; // 不存在增广路,返回失败
}
##例题
hdu2063
参考代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 505;
vector<int>vec[maxn];
bool vis[maxn];
int match[maxn];
bool find(int x){
for(int i : vec[x]){
if(!vis[i]){
vis[i] = 1;
if(match[i] == 0 || find(match[i])){
match[i] = x;
return true;
}
}
}
return false;
}
int main(){
int k;
while(cin >> k && k){
int n, m; cin >> n >> m;
for(int i = 1 ; i <= n ; i++)vec[i].clear();
memset(match, 0, sizeof match);
int u, v;
for(int i = 1 ; i <= k ; i++){
cin >> u >> v;
vec[u].push_back(v);
}
int ans = 0;
for(int i = 1 ; i <= n ; i++){
memset(vis, 0, sizeof vis);
ans += find(i);
}
cout << ans << endl;
}
}