匈牙利算法——你一定可以看懂的图论算法

匈牙利算法

第一次讲解一个算法,也是最近才学到的一个算法,我的语言表达能力可能不是很强,排版也不是特别好,但是真的有认真在做一篇文章,如果有好的意见可以告诉我,非常感谢。

匈牙利算法是一种在多项式时间内求解任务分配问题的组合优化算法,并推动了后来的原始对偶方法。美国数学家哈罗德·库恩于1955年提出该算法。此算法之所以被称作匈牙利算法,是因为算法很大一部分是基于以前匈牙利数学家Dénes Kőnig和Jenő Egerváry的工作之上创建起来的。(来源于百度百科)
但是匈牙利算法的作用不仅仅于此,它通常在图论中起着很大的作用,是图论中寻找最大匹配的算法,应用性十分强。

在正式了解匈牙利算法之前,我们必须先知道什么是二分图:又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
如图所示 在这里插入图片描述
这就是一个典型的二分图,我们区别二分图,通常要看它是否能分成两个独立的点集。二分图是这样一个图: 有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接!
无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。易知:任何无回路的的图均是二分图。
我们来看两个例子:
如上图,U和V构造的点集所形成的循环圈不为奇数,所以是二分图。
在这里插入图片描述
如上图,左右两边构成的点集并不独立,或者说构成的循环圈为奇数,这明显不是二分图。

了解完二分图之后我们再看看什么是匹配,最大匹配以及完美匹配。
匹配:在图论中,一个匹配是一个边的集合其中任意两条边没有公共顶点。

最大匹配:选择这样的边数最大的子集称为图的最大匹配问题(maximal matching problem)
完美匹配:如果一个匹配中,图中的每个顶点都和图中某条边相关联,则此匹配称为完美匹配。
那么在Fig.3和Fig.4就是Fig.2的匹配。而Fig.4为最大匹配。
如果存在完美匹配,那么完美匹配必定是最大匹配。

那么,我们的问题就是找最大匹配,借助的就是匈牙利算法或者最大流。
我们在此详解匈牙利算法。

该算法的核心就是寻找增广路径,用增广路径来求二分图的最大匹配的算法。哎,又到了我们最不喜欢的名词解释环节:

增广路径的定义是:
若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径(举例来说,有A、B集合,增广路由A中一个点通向B中一个点,再由B中这个点通向A中一个点……交替进行)。

在这里插入图片描述
如图所示就是一条增广路径,我们可以形象称之为“交错轨”。
结论总结
由增广路的定义可以推出下述五个结论:
1、P的路径长度必定为奇数,第一条边和最后一条边都不属于M。
2、不断寻找增广路可以得到一个更大的匹配M’,直到找不到更多的增广路。
3、M为G的最大匹配当且仅当不存在M的增广路径。
4、最大匹配数M+最大独立数N=总的结点数。
5 、 二分图的最小路径覆盖数 = 原图点数 - 最大匹配数。
为什么增广路径是最大匹配数这里不给予证明,还请读者自寻查阅相关书籍。

那么我们又该怎么寻找增广路径呢?
我们通过一个有趣的问题来解决。
通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有N个剩男,M个剩女,每个人都可能对多名异性有好感(暂时不考虑特殊的性取向),如果一对男女互有好感,那么你就可以把这一对撮合在一起,现在让我们无视掉所有的单相思(好忧伤的感觉快哭了),你拥有的大概就是下面这样一张关系图,每一条连线都表示互有好感。
在这里插入图片描述
本着救人一命,胜造七级浮屠的原则,你想要尽可能地撮合更多的情侣,匈牙利算法的工作模式会教你这样做:

、先给1号男生找妹子,那么我们发现他和1号女生互有好感且1号女生名花无主,我们就先给他们配对。连上一根蓝线。
在这里插入图片描述
、接着给2号男生找妹子,我们发现他和2号妹子互有好感且2号妹子名花无主,我们则可以给他们配对,OK,连上一根蓝线。

在这里插入图片描述
、接下来给3号男生找妹子,发现3号男生与1号女生虽然互有好感,可她已经名花有主了,我们寻找到她的对象1号男生,我们就让1号男生让一下吧,让她去找别的女生,找不到就没办法了。
在这里插入图片描述
则我们就先暂时取消他们的配对,让1号男生去看看有没有别的女生。
而1号男生与2号女生虽有好感,但2号女生已经名花有主,同样,我们让2号男生也先去找别的女生,看看能否给兄弟留点机会。就先暂时取消他们的配对。
在这里插入图片描述
此时2号男生发现3号女生还未名花有主,且他们互有好感,那么2号男生即可给1号男生腾空位,1号男生则也可给3号男生腾空位,则之前的问题迎刃而解!

扫描二维码关注公众号,回复: 11475386 查看本文章

2号男生与3号女生配对
在这里插入图片描述
1号男生与2号女生配对
在这里插入图片描述
3号男生与1号女生配对
在这里插入图片描述
则最终结果为:
在这里插入图片描述
皆大欢喜!

、终于到了我们的4号男嘉宾了,4号男生与3号女生有好感,可3号女生已经名花有主,那么我们按照第三步的方法就是给他腾空位。找到与3号女生配对的男嘉宾,让他去寻找别的女生,然后再依次这样,可我们发现无论如何都腾不出空位了,我们是在是没有办法,只能说:4号男嘉宾走好吧······

==================================================以上就是匈牙利算法的流程,这是一个递归的过程,我们本着有机会就上,没机会就创造机会,若没办法则无能为力的原则,总结为八个字:先来先得,能让则让
我们发现依照我们上述的方法寻找出来的就是一条增广路径。

OK,我们已经有了很多思路,那么我们就来编写代码来实现吧!

1.存储方式。

int n,m,t;//n代表男生人数,m代表女生人数。t表示边数,即有多少互感的数目。
int temp1,temp2;//临时变量,存放相配对的男生与女生。
const int maxn=100; //最大的男女生数。
bool line[maxn][maxn];      //用邻接矩阵来存储男生和女生的好感关系,行代表男生,列代表女生,有则为true,无则false。
bool used[maxn];
//判断女生是否名花有主。true则有,false则无。
int nex[maxn];//存储与女生配对好了的男生,便于待会腾空位。若没有,则为0,否则为男生的序号。

2.match()方法

int match()  //配对函数。
{
	int sum=0; //表示最大匹配数。
	for(int i=1;i<=n;i++)
	{
		memset(used,false,sizeof(used));//初始化,因为对于每个为配对男生来说,他们可能都名花无主。
		if(Find(i))sum++;         //如果能配对,则统计加1;
	}
	return sum;                  //配对完成,返回结果。
}

3.那么我们的关键就是Find()方法,如何发现可以配对,则就有很多要点了,我们通过上述可知,若名花无主,我们则可以直接上,但我们判断名花无主可不仅仅只是根据used来判断的,还有就是nex[]或者根据递归来腾位置,判断可不可以腾位置。

bool Find(int x)
{
	for(int i=1;i<=m;i++)//遍历m个女生
	{
		if(line[x][i]&&!used[i])//如果互有好感且女生名花无主(事实上并不是真正的名花无主,只是针对腾位置的,因为刚进入循环时used数组是被初始化的),则进行下一步。
		{
			used[i]=true;//我们则让她名花有主。
			if(nex[i]==0||Find(nex[i]))
			{
				nex[i]=x;//保存与该女生配对的男生序号。
				return true;//配对成功!
			}//如果女生没有配对男生或者跟女生配对的男生可以腾位置,则我们就OK了。
		}
	}
	return false;//配对失败!
}

关键的步骤都已实现完,我们来看完整代码。

#include<iostream>
#include<queue>
#include<algorithm>
#include<stack>
#include<cmath>
#include<cstdlib>
#include<set>
#include<vector>
#include<cstdio>
#include<memory.h>

using namespace std;

int n,m,t;//n代表男生人数,m代表女生人数。t表示边数,即有多少互感的数目。
int temp1,temp2;//临时变量,存放相配对的男生与女生。
const int maxn=100; //最大的男女生数。
bool line[maxn][maxn];      //用邻接矩阵来存储男生和女生的好感关系,行代表男生,列代表女生,有则为true,无则false。
bool used[maxn];
//判断女生是否名花有主。true则有,false则无。
int nex[maxn];//存储与女生配对好了的男生,便于待会腾空位。
bool Find(int x)
{
	for(int i=1;i<=m;i++)//遍历m个女生
	{
		if(line[x][i]&&!used[i])//如果互有好感且女生名花无主(事实上并不是真正的名花无主,只是针对腾位置的,因为刚进入循环时used数组是被初始化的),则进行下一步。
		{
			used[i]=true;//我们则让她名花有主。
			if(nex[i]==0||Find(nex[i]))
			{
				nex[i]=x;//保存与该女生配对的男生序号。
				return true;//配对成功!
			}//如果女生没有配对男生或者跟女生配对的男生可以腾位置,则我们就OK了。
		}
	}
	return false;//配对失败!
}
int match()  //配对函数。
{
	int sum=0; //表示最大匹配数。
	for(int i=1;i<=n;i++)
	{
		memset(used,false,sizeof(used));//初始化,因为对于每个为配对男生来说,他们可能都名花无主。
		if(Find(i))sum++;         //如果能配对,则统计加1;
	}
	return sum;                  //配对完成,返回结果。
}
int main()
{
	while(cin>>t)
	{
		cin>>n>>m;
		memset(line,false,sizeof(line));
		memset(nex,0,sizeof(nex));
		for(int i=0;i<t;i++)
		{
			cin>>temp1>>temp2;
			line[temp1][temp2]=true;
		}
		cout<<match()<<endl;
	}
	return 0;
}

我们拿上面例子案例做测试。
输入:
7
4 4
1 1
1 2
2 2
2 3
3 1
3 2
4 3
输出:
在这里插入图片描述
结果是没问题的,我们为其配对做月老也是十分合格的。
那么以上就是匈牙利算法的思路和代码实现,当然,它可不仅仅只是单独使用,通常是结合多种套路来实现的,有的时候你甚至发现不了原来这是是要用匈牙利算法的。当然,不要泄气,我们一起进步,多做就会有收获,俗话说孰能生巧嘛,我也会经常发一些算法题,供你们学习。

猜你喜欢

转载自blog.csdn.net/hzf0701/article/details/107339551
今日推荐