匈牙利算法

匈牙利算法用来解决二分图最大匹配问题。一个典型的最大匹配问题的描述如下:一个公司有n项工作,m个员工。每个员工能胜任n项工作中的几项(0~n)工作。问题是,如何分配才能使得被处理的工作数最大。当然,如果公司里人员很多,每项工作都有很多员工可以胜任,那么使每项工作都有人处理的方案是显而易见的。但遇到人员稀少,几个项目可能只有一个人才能胜任的情况下,问题不再简单。假如A能处理a,b问题,B能处理b,这时如果让A处理了b,a便没人胜任了。当问题扩充到很大时,将会是很麻烦的问题。

二分图:二分图是一种特殊的图。图中的顶点可以分为两个不相交的集合(A,B)。且图中每条边两端的顶点分别属于两个集合。在上面的例子中,员工集合是A,工作集合为B。员工胜任工作的对应关系即为边的关系。

最大匹配:二分图G的一个子图M中,如果任意两条边没有公共顶点,或者说没有一个顶有条路,则称其为G的匹配。对于上例中,只要分配方案中,一个工作人员只完成一项工作,一项工作只由一个人员完成,则称为一个匹配。最大匹配是指在所有匹配中,边数最多的一种匹配。即能完成工作最多的分配方案或者参加工作人员最多的方案。如果匹配中所有顶点都和某个边连着,则称为完美匹配完备)。

简单的看,求最大匹配的方法就是找出所有的匹配,然后找出匹配中最多边数的匹配,即为最大匹配。不过这样算法的时间复杂度大的让人发指。匈牙利算法又称增广路法。增广路是该算法的精髓。

增广路:这个定义维基给的比较水“若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。”读了很多遍都感觉很饶。增光路的存在是相对某个给定的匹配(M)。路径P是增广路的充分必要条件:

1.起点和终点都不属于M。

2.P中任意两个相邻的边,一个属于M,一个不属于M。

由此,可以推出增广路的几条性质:

1.P的路径长度必定为奇数,第一条边和最后一条边都不属于M。

2.将M和P进行异或操作(去同存异)可以得到一个更大的匹配M’。

3.M为G的最大匹配当且仅当不存在M的增广路径。

回到匈牙利算法,由2,3性质,我们只要找到图的一条增广路作为一个新的匹配继续找其增广路,重复下去最后总能找到其最大匹配。


原理搞清楚了,接下来就是代码实现了:

/*以下代码出自 www.nocow.com*/
#include<stdio.h>
#include<string.h>
bool g[201][201];      //图  
int n,m,ans;             
bool b[201];           //访问标记  
int link[201];         //B集合中与A集合中匹配的顶点  
   
bool init()            //录入图(这个录入有些神奇,搞不懂...无视之...)  
{  
        int _x,_y;  
        memset(g,0,sizeof(g));  
        memset(link,0,sizeof(link));  
        ans=0;  
        if(scanf("%d%d",&n,&m)==EOF)return false;  
        for(int i=1;i<=n;i++)  
        {  
                scanf("%d",&_x);  
                for(int j=0;j<_x;j++)  
                {  
                        scanf("%d",&_y);  
                        g[ i ][_y]=true;  
                }  
        }  
        return true;  
}  
   
bool find(int a)                                            //找从a出发的增广路,a将是集合A的遍历  
{  
        for(int i=1;i<=m;i++)                               //*1遍历集合B 
        {  
                if(g[a][ i ]==1&&!b[ i ])                   //*2如果a与Bi有通路
                                                            //且Bi在a作为起点的循环中没有被访问过  
                {  
                        b[ i ]=true;                        //*3  
                        if(link[ i ]==0||find(link[ i ]))   //*4 Bi没有匹配(即已经找到一条增广路)
                                                            //或能递归的找到一条增广路  
                        {  
                                link[ i ]=a;                //*5  
                                return true;  
                        }  
                }  
        }  
        return false;  
}  
   
int main()  
{  
        while(init())  
        {  
                for(int i=1;i<=n;i++)                       //*6  
                {  
                        memset(b,0,sizeof(b));              //*7
                        if(find(i))ans++;                   //*8每次find返回true说明找到一条增广路,
                                                            //且增广路边数逐一递增。  
                }  
                printf("%d\n",ans);  
        }
}
 

解释别人的代码是件很繁琐的事情。理解是一个步骤,讲出来又是一个步骤。递归程序加注释难以解释清楚。
程序的流程是遍历A集合顶点 (*6)因为是从A中遍历,所以每一次循环出的点Ai一定是未匹配的点,即一条增广路的起点。find()的递归停止条件分别在(*2)(*4)。如果返回false则说明从Ai出发没有增广路。如果返回ture要么是(*4)中link[i]==0,即发现未匹配的点,以该未匹配的点作为增广路的结尾。要么递归的find(link[i])返回了ture。再来分析递归的find(link[i])。link[i]是与Bi匹配的Aj,如果开始递归find(link[i]),说明前面(*3)的条件通过,Ai也与Bi通路。说明Aj与Ai共同连接着Bi,明显Aj已经参与过find()。重新调用find(Aj)事,唯一不同的是,Bi被标记为了已访问(*3)。说明本次调用Aj会忽略Bi重新找一条增广路。如果找到了,则Ai->Bi->Aj->P(由Aj出发的增广路)又构成一个增广路。其中,因为Aj,Ai都连着Bi,所以Ai->Bi->Aj的连通性是没有问题的。这样增广路的边数每次只增加1(其实如果递归调用了find()相当于拆掉之前匹配好的一条边,然后用Ai形成的边补回去,如果被拆的Aj没有通过拆别的边找到新的增广路,即(*3)是由条件link[i]==0结束,那么说明Aj找到了一个未匹配点,则边数+1,匹配数+1)。所以无穷无尽的递归说白了就是当前读入的点和以前的点有共同连接的顶点,如果以前的点能找一个新的匹配点则把当前点到增广路中。换到文章开始时的例子中就是先一个人一个任务的安排,如果找到一个人(A)能胜任的任务已经被别人(B)胜任,则看看B还有没有其他任务可以完成,如果有,则交换A接手B的任务,B去做其他任务,如果B没有任务可以完成了,则A放弃这个任务,再从下一个能完成的任务中找。没有可以胜任的任务时,被完成的工作数不增加。反之说明匹配成功一次。任务+1。最后统计出的数据一定是达匹配数。


恩...绞尽脑汁的把代码翻译了一遍...感觉还是没说清楚。算了,明天手写一遍程序就好。恩恩...
选拔赛就开始了啊,黎琦大神我拜拜你了,再别copy代码了好不好,到时候抢了人强队的名额可就是罪过了...诶...蛋疼孩子没法说啊,没法说。啧啧啧~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值