【序言】
还记得自己当初学二分图匹配的时候……(哇,感觉时间过得好快好快啊)我翻看了很多的资料,不知道是不是因为我笨这个非常特殊的原因,我硬是看不下满版的公式定理概念证明代码符号等等无节操的东西,太深有感触了,就一个这么容易的东西讲那么复杂干嘛,于是我还是自己写一个作纪念好了,这里没有“X部图”“Y部图”“增广路”!这里只有那只可爱无下限的小白兔!!
【警告】
你是人!请不要把自己当做程序员一样的思考!你只是人!请像正常人一样的思考!
【一】
从前,在大森林里面,有一群可爱的小白兔,为什么叫它们可爱的小白兔呢?因为它们非常的可爱。
兔子们转眼都长大了,长大了就要结婚,作为兔子国的月老(不要推辞了就是你!),你的任务是钦点夫妻,当然你已经知道了它们的恋爱关系,如下图所示。为了兔子王国的未来,同样作为兔子的你,该怎么钦点才能让夫妻最多呢?(假设你是一只有良知的兔子)
Ps.为了表示对于崇高的兔子的尊敬,我们用框框来代表兔子,并且用男女表示性别。
哈哈哈哈,我已经听到了你的笑声,作为一只聪明的兔子,你一下就分出来了,哇塞!好厉害啊!(哼哼,等一下,难道有人不会么!!(愤怒))
【二】
又过了好多好多年,兔子也开始像人类一样无节操了,它们!它们!它们!竟然妄想一夫多妻或者是一妻多夫!!天哪,我的三观!求求你快来拯救兔子世界。
我想你已经看到了,作为一个正常的人类,我只能为你这只兔子而感到悲哀!这也太不像话了!(难道女兔2号就是传说中的白富美?)
其实这个问题交给任何一个正常的人,他都会这样做:
男兔1号,我命令你娶了女兔1号,OK,一边去。
男兔2号,我命令你娶了女兔6号,OK,一边去。
男兔3号,我命令你娶了女兔2号,OK,一边去。
男兔4号,我命令你娶了……不好,你的女朋友不要你了,好的,你可以走了。
男兔5号,我命令你娶了……怎么又被抢了,白富美热销啊,你走吧。
男兔6号,我命令你娶了……白富美没了,你娶4号吧,OK,一边去。
完了?没有完啊!日子不好过啊,男兔4号与5号斧头刀子砸你家门啊,谁要你没有满足他们的心愿!
于是你开始追根溯源,原来它们喜欢的是白富美2号!让我看看白富美被谁抢了呢?啊,原来是男兔3号,于是你开始调查男兔3号的恋爱背景,秘密就这么被你发现了,它竟然对白富美不忠心!它还喜欢没人要的女兔5号!天哪,这么不忠诚的人有什么资格娶白富美小姐呢!速度贬下去,钦点给没人要的女兔5号以示惩罚,那么现在争抢的就只有尊贵的白富美小姐了,而且追求者们还是那么的忠诚,心儿都在它的身上,绝无杂念,这可怎么是好呢,作为一名非常公平的月老兔子,你决定把白富美送给4号,谁要4号给你送礼了呢!于是乎,出现了5对夫妻,比刚才可多了1对,功劳啊功劳,还能再多吗?我想是不行了,那么就这么定了吧。
这么个顺利成章满足正常人思维的分配方式,没错,它就是大名鼎鼎的匈牙利算法。
不过跟正常人思维有丁点儿不一样的是,它真正实现的时候,是在发现男兔4号的女朋友被抢了后就立马开始调查3号的身份的,而不是像上面描述的那样等到所有的人都分配完,并且家门被砸了后再开始调查,除开这一点不一样,想法都是一模一样的了。
1 function find(i:longint):boolean; // 上文所述的对一只兔子的恋爱背景进行检查过程 2 var 3 j,k:longint; 4 begin 5 if p[i] then exit(false); // 如果这只兔子在这轮的钦点中已经被检查过背景了,就退出 6 p[i]:=true; // 提前将这只兔子标记为检查完毕 7 k:=h[i]; // 邻接表操作,获取该兔子的恋爱信息 8 while k<>0 do begin 9 j:=g[k]; // 获取恋爱对象 10 if (y[j]=0)or(find(y[j])) then begin // 如果心仪对象单身或者其丈夫不忠诚而被月老贬给了别人 11 x[i]:=j;y[j]:=i;exit(true); // 调整夫妻关系,返回一个成功信息 12 end; 13 k:=next[k]; // 若该轮不成功,物色下一个恋爱对象 14 end; 15 exit(false); // 反正就是不成功,没办法了 16 end;
注意事项:
1、x[]记录的是男兔的妻子,y[]记录的是女兔的丈夫
2、出于对女性兔子的尊重,月老只会检查男性兔子的恋爱背景
3、返回的不成功信息有多重含义
我知道你觉得莫名其妙,不要急,往下看……
step1、标记男兔1号检查完毕,找到女兔1号配对
step2、清空所有检查完毕标记
step3、标记男兔2号检查完毕,找到女图6号配对
step4、清空检查完毕标记
step5、标记3号男兔检查完毕,找到2号女兔配对
step6、清空检查完毕标记
step7、检查男兔4号检查完毕,欲配对女兔2号
step8、标记男兔3号已检查,开始检查3号是否忠诚
step9、发现3号不忠诚,将其贬给女兔5号,之后将女兔2号赐给男兔4号,并清除标记
step10、标记男兔5号为检查完毕,欲使女兔2号与其配对
step11、发现女兔2号已经被分配,检查其丈夫4号是否可以迁就
step12、男兔4号忠心耿耿无法迁就,男兔5号配对失败,清除标记
step13、标记男兔6号为检查完毕,欲使女兔2号与其配对
step14、发现女兔2号已经被分配,检查其丈夫是否可以迁就
step15、男兔4号忠心耿耿无法迁就,配对失败,男兔6号试探下一个对象女兔4号成功
step16、清除检查完毕标记
至此,通过伟大的匈牙利算法,最大的匹配方案就出来了,这与我们人的思维是大题一致的。
匈牙利算法有一个重要的细节,那就是不停标记,再在一次匹配成功后清除所有标记,在刚才的算法模拟中,似乎我们并没有看到标记起了任何作用,那标记的作用在哪里??
不急,且看下面这种情况:
目前正在为2进行配对检查,欲赐给它的是女兔1号,然而女兔1号已有丈夫男兔1号,按照算法流程,接下来将检查男兔1号的忠诚与否,这个检查过程是怎样的呢?其实就是把男兔1号当成是正在进行配对检查的男兔2号一样,看看若女兔1号不存在,男兔1号还能找到别的归宿吗?然而不可避免的是,男兔1号在检查的时候又会找到女兔1号,按照算法流程,它又会继续检验男兔1号是否能找到新的归宿,这就避免不了出现死循环!按照我们刚才的要求,是不允许男兔1号继续访问女兔1号了,因为此时我们不承认女兔1号的存在,那么我们在访问到男兔1号时,就将其标记为已检查,再次通过女兔1号而访问到男兔1号时,根据代码第一句话可知,算法将发现男兔1号已访问,于是自动退出女兔1号的访问过程,从而屏蔽了继续对女兔1号的访问。
标记法的核心在于:我们访问女兔,并不是为了访问女兔,只是希望通过访问女兔来间接访问到它的丈夫,如果它的丈夫已经标记为不可继续访问,那么相当于这只女兔被标记为不可继续访问,从而优美地实现了屏蔽。
啊,兔子活得真累,我觉得我也是,匈牙利算法就这么多了,其实多想想挺好理解的。
对于二分图匹配,还有一个更加高效的算法,那就是HK算法,当然它的思想还是跟匈牙利一样,只不过进行了标号优化,这里就不赘述了。
完