最近在参加了一个小项目,里面用到二分图匹配算法,因为之前并没有接触过相关算法,于是找到一些博客进行了一番学习,但学习之后,发现部分博客或多或少存在一些另我疑惑的地方,于是,我打算写此博客以巩固对算法的理解。
一、二分图基本概念
二分图(Bipartite graph)又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
通俗一点,二分图是一类特殊的图,它可以被划分为两个部分,每个部分内的点互不相连。
如下图所示,可以将【2】【4】
结点视为一个集合,【1】【3】【5】【6】
结点视为一个集合,集合内部的结点互不相连,因此该图为一个二分图。
重要定理:G为二分图的充要条件是G中的每一个圈的长度都是偶数。
二、二分图匹配相关概念
1、什么是匹配?
注意,本文只讨论二分图的最大匹配,不讨论最优匹配。
给定一个二分图G=(V,E),在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配;具有边数量最多的匹配称为最大匹配;若|V|=2|M|,则称M为完美匹配。
如下所示为一个二分图的完美匹配:
二分图完美匹配一定是最大匹配,最大匹配不一定是完美匹配。
2、什么是交替路与增广路?
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路(Augmenting Path):从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发点不算),则这条【交替路】称为增广路。
增广路的三个性质:
- M为G的最大匹配当且仅当不存在M的增广路径。
- 增广路P的路径长度必定为奇数,第一条边和最后一条边都不属于M,不匹配的边比匹配的边多一。
- 不断寻找增广路可以得到一个更大的匹配M’,直到找不到更多的增广路。
增广路的这三个性质可以用来寻找匹配,在接下来会说明。
如下图所示:
路径2
->A
->1
->B
为一条交替路,因为出发点2
和途径点B
均为未匹配点,因此该路径也为增广路。
三、匈牙利算法
1、基本概念
匈牙利算法的核心就是寻找增广路。具体而言,匈牙利算法从二分图中没有匹配的点开始寻找增广路径(增广路径性质3)。找到增广路后,根据增广路径的性质,显然路径里没被匹配的连线比已经匹配了的连线多一条(增广路径性质2),于是对增广路径中的边进行取反操作,这样匹配数就比原来多1个。不断执行上述操作,直到找不到增广路径为止,无法找到增广路径说明已达到最大匹配(增广路径性质1)。
取反操作:将增广路的匹配边变成不匹配边,不匹配边变成匹配边。
如上图所示:
- 首先从
未匹配点1
开始寻找增广路,找到了另一个未匹配点A
说明找到了增广路,停止寻找,进行取反操作,将非匹配边变为匹配边,于是1-A
为匹配边。 - 从
未匹配点2
开始寻找增广路,显然2->A->1->B
为增广路,进行取反,于是此时匹配边为2-A、1-B
。 - 从
未匹配点3
开始寻找增广路,显然3->A->2->C
为增广路,进行取反,于是匹配边变为3-A、2-C、1-B
。因为已经不存在未匹配点,此时已经找不到更多增广路径,因此达到了此二分图的最大匹配。
注:显然,二分图匹配结果并不唯一。
以上就是匈牙利算法的过程,很明显,搜寻增广路径这个步骤是基于深度优先搜索的。
匈牙利算法寻找增广路的伪代码描述:
//以下为寻找增广路并进行取反操作的伪代码,取反操作通过递归调用完成。
while(找到Xi的关联顶点Yj){
if(顶点Yj不在增广路径上){
将Yj加入增广路;
//如果Yj是未匹配点说明成功找到增广路,从“Yj处继续寻找能够找到增广路”属于递归调用本函数过程,进行取反操作
if(Yj是未匹配点||从Yj处继续寻找能够找到增广路){
将Yj的匹配点改为Xi;
return true;
}
}
return false;
}
2、匈牙利算法的实现
网上搜索匈牙利算法,从思想上看都是基于dfs的,另外,我注意到也有一些博客提出了基于广度优先(bfs)的匈牙利算法,我不确定所谓“基于bfs的匈牙利算法”能否称为匈牙利算法,我个人的理解是匈牙利算法就是基于dfs的来寻找增广路径而实现的。
2.1、实际问题
问题描述
RPG girls今天和大家一起去游乐场玩,终于可以坐上梦寐以求的过山车了。可是,过山车的每一排只有两个座位,而且还有条不成文的规矩,就是每个女生必须找个个男生做partner和她同坐。但是,每个女孩都有各自的想法,举个例子把,Rabbit只愿意和XHD或PQK做partner,Grass只愿意和linle或LL做partner,PrincessSnow愿意和水域浪子或伪酷儿做partner。考虑到经费问题,boss刘决定只让找到partner的人去坐过山车,其他的人,嘿嘿,就站在下面看着吧。聪明的Acmer,你可以帮忙算算最多有多少对组合可以坐上过山车吗?
输入:
输入数据的第一行是三个整数K , M , N,分别表示可能的组合数目,女生的人数,男生的人数。0<K<=1000
1<=N 和M<=500.接下来的K行,每行有两个数,分别表示女生Ai愿意和男生Bj做partner。最后一个0结束输入。
输出:对于每组数据,输出一个整数,表示可以坐上过山车的最多组合数。
样例输入:6 3 3 1 1 1 2 1 3 2 1 2 3 3 1 0
样例输出:3
这道题本质上就是求二分图的最大匹配问题。
2.2、代码实现
Java实现:
此代码转载自https://wmathor.com/index.php/archives/1344/,//***//
表示是我添加的注释。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int[][] map;
static int n, m;
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
while (cin.hasNext()) {
int t = cin.nextInt();
if (t == 0)
break;
n = cin.nextInt();
m = cin.nextInt();
map = new int[n + 1][m + 1];
for (int i = 0; i < t; i++)
map[cin.nextInt()][cin.nextInt()] = 1; // 有向边
int count = 0; // 最大匹配数
int[] mc = new int[m + 1]; // mc[i] = j 表示i号男生所连的女生是j号
Arrays.fill(mc, -1); // 初始时所有女生都没有连
//***//以下for循环是核心部分,表示依次从未匹配点开始寻找增广路径
for (int i = 1; i <= n; i++) {
//***//将增广路径中的结点记录下来,以免重复,因此每一次循环都必须重置。
boolean[] vis = new boolean[m + 1]; // vis[i] = true 表示i号男生已经被匹配了
//***//根据增广路径性质2、3,每找到一条增广路径就说明找到了一条匹配边
if (dfs(i, vis, mc))
count++;
}
System.out.println(count);
}
}
//***//注:这个是寻找增广路径的代码,可以结合之前的伪代码,每找到一条增广路径就说明找到了一条匹配边。
private static boolean dfs(int start, boolean[] vis, int[] mc) {
for (int i = 1; i <= m; i++) { // 枚举男生集
if (!vis[i] && map[start][i] == 1) { // 如果这个男生没被匹配并且和当前的start有边相连
vis[i] = true; // 将这个男生标记为匹配过
//***//以下判断语句中dfs递归调用,对应匈牙利算法中寻找增广路径并取反的操作,大家可以试着手动推理一下
if (mc[i] == -1 || dfs(mc[i], vis, mc)) { // 这个男生没有和女生匹配 || 这个男生所连的女生还有别的选择,就把这个男生"腾"出来
mc[i] = start;
return true;
}
}
}
return false;
}
}
Go:
有时间再把go的代码实现并添加过来,先画个饼。
四、总结
匈牙利算法的核心在于找增广路径,注意增广路径的三个性质。文中伪代码部分可以当成是解决这类问题的模板。
本文内容也是我自己对于算法的理解,可能存在错误,欢迎批评指正。
五、参考博客
https://blog.csdn.net/lemonxiaoxiao/article/details/108672039