前言
决心着手一个新的专题,结果不小心把自己辛辛苦苦写了几个小时的博客给删除了。希望同学们不要辜负我的一片苦心。
二分图匹配问题其实是完全可以用最大流或者是最小费用最大流来解决,本文想介绍一种比以上两种要快的方法——增广路算法(看不懂这句话的同学可以忽略这句话)。
友情链接:Leo’s [UOJ78]二分图最大匹配 机房学长~ Orz
这例题出得很好嘛:[UOJ78]二分图最大匹配 神清气爽的题目~
1.二分图和增广路定理
首先,什么是二分图呢?
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。——360百科
(定义的什么玩意?!垃圾!)
对于一张无向图,我们可以把它的所有结点分成两类,我们把它们称为“X类结点”和“Y类结点”。如果在这张图中的所有边连接的两个节点都满足一个是“X类结点”、另一个是“Y类结点”,那么我们就称这个图为“二分图”。(简洁明了!)
在给同学们介绍“增广路算法”之前,先要给大家介绍“增广路定理”。“增广路算法”的目的是求一个“二分图的最大匹配”。而什么是“二分图的最大匹配”呢?举一个形象的例子:总理同学是一个非常不拘小节的(不修边幅)人,他用圆珠笔从来想不起来盖笔帽,而且他的笔的类型很杂跟本看不出哪个笔帽对应的是哪个笔(甚至有一些笔和笔帽已经丢了)。我们就此建立一张二分图,“X类结点”表示笔帽,“Y类结点”表示笔杆。图中的“边”表示所连接的笔和笔帽可以盖在一起(显然,不同牌子的笔帽和笔盖也可能可以盖在一起)。我们要找到一种匹配方式为尽可能多的笔配上笔帽,每支笔只能配一个笔帽。我们要找的答案就是这个“二分图的最大匹配”。如果能找到一种方法使得所有的笔和笔帽都能得到匹配,“那真是非常的完美啊!”,所以我们称这种“完美的情况”叫做“二分图的完美匹配”。
在给大家介绍“增广路定理”之前,先要给大家介绍什么是“增广路”。假设现在总理同学正在一点一点地给自己的笔帽配对,而且已经尝试着匹配了一部分笔和笔帽(但是不一定是“二分图最大匹配”中的匹配方式)。如果一个点(“笔帽”或是“笔杆”)到目前为止还没有得到匹配,那么我们称它为“未盖点”。在当前的这个状态下,已经进行了匹配的“笔帽”与“笔杆”之间的边叫做“匹配边”(匹配边一定是原图中的边),而原图中剩余的边我们叫它“未匹配边”。
(接下来是一个很不好理解的地方,请同学们打开脑洞。)
假设:“笔帽A”是一个“未盖点”,“笔帽A”可以和“笔杆X”配对,但是“笔杆X”已经和“笔帽B”配对。“笔帽B”可以“笔杆Y”配对,“笔杆Y”又是一个“未盖点”。下图数对这个局部的图的描述,虚线表示未匹配边,实现表示当前匹配边:
总理惊奇的发现了一个事实:如果把上图中的所有“边的状态”反转(也就是“未匹配边”变成“匹配边”,“匹配边”变成“未匹配边”)之后整个图似乎仍然是成立的。而且最重要的是,原来有只有一对匹配,而现在现在有两对(比原来多了一对)。
像这种“未盖点”->“未匹配边”->“匹配边”-> … ->“未匹配边”->“匹配边”->“未匹配边”->“未盖点”的路径就叫做“增广路”。每当我把一条“增广路”进行反转时“匹配边”就会增加一条,最后逐渐趋近“最大匹配”。(值得注意的是,这个理论同样适用于不是“二分图”的图,但是我们今天值讨论“二分图”。)
2.匈牙利算法
匈牙利算法就是一种通过“增广路”来计算二分图最大匹配的算法。
匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名。匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法。
然后就又出现了一个新的概念叫做Hall定理:
Hall定理:
此定理使用于组合问题中,二部图G中的两部分顶点组成的集合分别为X, Y, X={X1, X2, X3,X4,………,Xm}, Y={y1, y2, y3, y4 ,………,yn},G中有一组无公共点的边,一端恰好为组成X的点的充分必要条件是:
X中的任意k个点至少与Y中的k个点相邻。(1≤k≤m)
看不懂没关系,我给同学们解释。假设整张“二分图”有一个局部:
只看我框中的x1、x2、x3和y1、y2。由x1、x2、x3引出的所有的边都指向了y1、y2,由此证明:这张图没有“完美匹配”。因为在这样一种情况中x1、x2、x3中至少有一个点得不到匹配。假如x1与y1匹配,x2就不能进行匹配,假如x1与y2进行匹配,x3就不能进行匹配。假如y2与后面灰色的结点匹配,那么可以与x1、x2、x3匹配的结点就更少了。由此可以推得:如果一个图存在“完美匹配”, X中的任意k个点至少与Y中的k个点相邻。这就是Hall定理的内容。
现在我们讲一下匈牙利算法具体要怎么写,我想借助于前面的那道例题和“Leo学长”的代码来讲一讲,这是那道例题:
真是一个非常感人的题面,然后我们再看一看,样例数据:
数据范围如下:
我给大家解析一下“Leo”的代码(其实并不是很好理解):
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define maxn 503
int nl,nr,m,ans;//nl男生数量,nr女生数量,m"二分图"边数,ans表示CP总数
int match[maxn];//match[i]表示女生i正在和谁约会(约会不代表将来一定能成)
int anti[maxn];//用来计算男生i的匹配(match的反函数)
bool vzt[maxn];//用来记录一个女生是否已经处于约会状态
vector<int> e[maxn];//e[i]表示男生i追的所有女生~
bool find_match(int u)//为男生i找到匹配
{
int s=e[u].size();//求出他的追的人的总数
for(int i=0;i<s;i++)//对于所有他追的女生
{
int v=e[u][i];
if(!vzt[v])//如果还没有约会
{
vzt[v]=true;//男生向这个女生提出约会
if(!match[v]||find_match(match[v]))
{//如果这个女生还没有和任何人约会,或是现在与她约会的人可以找到其他的人约会
match[v]=u;//把当与前女生约会的人设成当前的这个男生
return true;//返回真,表示CP多了一对
//其原理在于:虽然形成新的CP可能会拆散原来的CP
//但是根据"增广边理论"CP数只增不减,要么+1,要么不变
}
}
}
return false;//返回该男生没有找到匹配
}
int main()
{
scanf("%d%d%d",&nl,&nr,&m);//输入男生人数,女生人数,还有"边"数
for(int i=1;i<=m;i++)//输入这m条边
{
int v,u;
scanf("%d%d",&v,&u);//输入这个男生和这个女生
e[v].push_back(u);//把这个女生加入他的"追随列表"
}
for(int i=1;i<=nl;i++)//对于每一个男生i
{
memset(vzt,0,sizeof(vzt));//初始化vzt数组
if(find_match(i))ans++;//如果i找到了匹配,总CP数+1;
}
printf("%d\n",ans);//输出CP总数
//到了这个时候,我们已经求出了最大匹配
//现在仍在保持约会状态的人结为配偶
for(int i=1;i<=nr;i++)
anti[match[i]]=i;
//match记录的是女生的匹配
//我们要把它转化成anti以记录男生的匹配
for(int i=1;i<=nl;i++)
printf("%d ",anti[i]);//按顺序输出每个男生的匹配
return 0;
}
3.稳定婚姻问题
其实就是把上文代码中的vactor改成一个priority_queue就好了!
实在是写了太长时间了,只好给读者留下一些思考空间了。
4.后记
上文未完待续…
赶稿匆忙,如有谬误,望同学们理解。