原题如下:
随着社会的不断发展,人与人之间的感情越来越功利化。最近,爱神丘比特发现,爱情也已不再是完全纯洁的了。这使得丘比特很是苦恼,他越来越难找到合适的男女,并向他们射去丘比特之箭。于是丘比特发了一封邮件给月下老人——掌管东方人爱情的神,向他求教。
月下老人回信告诉丘比特,纯洁的爱情并不是不存在,而是他没有找到。在东方,人们讲究的是缘分。月下老人只要做一男一女两个泥人,在他们之间连上一条红线,那么它们所代表的人就会相爱——无论他们身处何地。而丘比特的爱情之箭只能射中两个距离相当近的人,选择的范围自然就小了很多,不能找到真正的有缘人。
丘比特听了月下老人的解释,茅塞顿开,于是用了人间的最新科技改造了自己的弓箭,使得丘比特之箭的射程大大增加。这样,射中有缘人的机会也增加了不少。
当然,无论丘比特怎么改造自己的弓箭,总还是存在缺陷的。首先,弓箭的射程尽管增大了,但毕竟还是有限的,不能像月下老人那样,做到“千里姻缘一线牵”。其次,无论怎么改造,箭的轨迹终归只能是一条直线,也就是说,如果两个人之间的连线段上有别人,那么莫不可向他们射出丘比特之箭,否则,按月下老人的话,就是“乱点鸳鸯谱”了。
情人节(Valentine's day)的午夜零时,丘比特开始了自己的工作。他选择了一组数目相等的男女,感应到他们互相之间的缘分大小,并依次射出了神箭,使他们产生爱意。他希望能选择最好的方法,使被他选择的每一个人都被射中一次,且每一对被射中的人之间的缘分的总和最大。
这时丘比特发现了一个严重的问题,他想了好一阵子都想不出来,最后他决定打电话去问一下月下老人。
“我想让我选择的每一对男女的总缘分值最大,可是,有的男人同时跟多个女人的缘分值都很大,而有的男人跟所有的女人的缘分值都不大,即使是我对于每个男人都选择缘分值最大的女人来匹配,但是由于一个男人只能对应一个女人,最终得到的局部最优解也未必是全局最优解啊!”
“呵呵,”月下老人说,“你也意识到这个问题啦,想当年我为了这个问题也是头痛了很久。后来我听说计算机能解决复杂的问题,我就去北大修了个计算机双学位,后来自己编了个程序,把这个问题解决啦。”
丘比特就向月下老人要那个程序,但是月下老人说他的电脑恰好中了病毒,上不了网,没法把程序传给丘比特。这可把丘比特急的。
情急之下,丘比特只好到百度知道上发帖,悬赏10000分征求能够求出最佳匹配方案的程序。
现在你看到了丘比特的帖子,你的任务是帮丘比特找到最佳的方案。
输入文件第一行为正整数k,表示丘比特之箭的射程,第二行为正整数n(n=20),随后有2n行,表示丘比特选中的人的位置,其中前n行为男子,后n行为女子。位置是由一对整数表示的坐标,它们之间用空格分隔。剩下部分是一个n×n表格,第i行第j列的数表示第i个男子与第j个女子之间的缘分值,在0到255之间。
分析如下:
这是一个二分图,左边20个顶点代表男的,右边20个顶点代表女的,每一条连接左边顶点和右边顶点的边都有一个对应的权值代表他们的缘分值(用二维数组w[i][j]来表示),如果某一对男女因为射程或中间有人的限制而不可能成为一对,那么相当于把他们之间的边的权值设为负无穷。现在问题就是,如何求出连接两边的最佳的20条边(任意两条边不能有共同的顶点),使这20条边的权值之和最大。
那么有一个问题就是,你如何判断一个匹配方案是最佳的?
神奇的算法,(好像叫匈牙利算法,或KM算法),就是从这个问题入手的,如果我们能证明所有匹配方案的结果都小于某个值,而且恰好有一个方案的结果是这个值,那么这个方案就是最佳方案。
如果我们给每个顶点也分配一个权值(称为顶标),左边是lx[i],右边是ly[j],保证对于每一个i和j,都有lx[i]+ly[j]-w[i][j]>=0(下面把lx[i]+ly[j]-w[i][j]叫做边的判别式)。如果存在一个匹配,所有的边的判别式都等于0,那么这个匹配肯定就是原图中的最佳匹配。
但问题是,对于当前的顶标,并不一定存在全部边的判别式都等于0的匹配。这时,可以用某种方法对顶标进行调整。具体调整方法见后。现在看一下整体的思路(数学归纳法):
初始化:随便找到一组满足条件的lx[i],ly[j],这个并不难。
一:先在第一个男人与第一人女人之间寻找最佳匹配,(显然匹配方式只有一种,所以这一种就是最佳匹配)。
二:假设前x-1个男人与前x-1个女人已达到最佳匹配,那么现在要把男x和女x插入,这时男x有两种选择,要么直接跟女x,要么跟前x-1女中的某个(设为第y1个),如果他选择后者,那么这个女y肯定之前已经与某男(记为linky[y1])匹配,那么这个linky[y1]先生,如果他选择女x,那么递归至此结束,如果他选择的是前x-1中的某个y2,那么那位linky[y2]先生同样要面临选择……直到有人选择女x为止(在此之前linky[x]=-1)。
两男不能同选一女,所以我们用visy[y]来记录某女是否被选过,已被选过的设为true,则后面的先生们不会再选到她。同时我们用visx[x]来记录某男是否面临过选择(不管他选了没有),这个值在调整顶标的时候有用。
上面没有考虑到边的判别式是否为0,实际上,当某男临选择的时候,只有某个选择对应的判别式为0,他才可以选定。如果最终有一男选定了女x,那么关于前x男和前x女的最佳匹配已经找到。如果在选到女x前,中途有某男,他面临的所有选择的判别式都大于0,那么寻找最佳匹配就失败了,这时就需要对顶标进行调整。
首先确定哪一些顶标需要调整,那么那些面临过选择的先生们,他们的顶标要调低,这样他那些判别式大于0的选择就可能转换成等于0的,这样他们就可以有更多的选择。
但是在这个过程中,有一些女士已经被某些男士选定,即他们之间的判别式是0,如果男士们调低顶标的话,那么这些女士的顶标要相应调高,以保持判别式仍为0。
最后是确定调整的幅度,那么就要看男人们所面临的所有判别式大于0的选择中,哪一个的判别式最小,那么把此最小值(记为lack)作为调整的幅度。
程序最核心的代码如下:
const int maxn=20,OO=2147483647;
int w[maxn][maxn];
int lx[maxn]={0},ly[maxn]={0};
int linky[maxn];
int visx[maxn],visy[maxn];
int lack;
bool find(int x){
visx[x]=true;
for(int y=0;y<maxn; y++){
if(visy[y])continue;
int t=lx[x]+ly[y]-w[x][y];
if(t==0){
visy[y]=true;
if(linky[y]==-1||find(linky[y])){
linky[y]=x;
return true;
}
}
else if(lack>t)lack=t;
}
return false;
}
void KM(){
memset(linky,-1,sizeof(linky));
for(int i=0;i<maxn; i++)
for(int j=0;j<maxn; j++)
if(w[i][j]>lx[i])
lx[i]=w[i][j]; //初始化顶标
for(int x=0;x<maxn; x++){
for(;;){
memset(visx,0,sizeof(visx));
memset(visy,0,sizeof(visy));
lack=OO;
if(find(x))break;
for(int i=0;i<maxn; i++){
if(visx[i])lx[i]-=lack;
if(visy[i])ly[i]+=lack;
}
}
}
}
算法评价:
我想不到这样一个问题,竟然可以用这么少的代码就解决。而运行速度更是惊人的快,我把人数从20增加到50,再到100,所用时间仍然是0毫秒(不计前面的判断两人距离以及两人之间是否有人的时间),结果我不想再试了。后来用了一个计数变量,得出此算法的时间复杂度大概在n的平方与三次方之间。