这个是在工做中遇到的一个实际的算法问题,问题描述以下,当前有m个司机,n个乘客,每一个司机和每一个乘客的距离由经纬度能够计算获得,如何匹配可使其去接乘客的距离和最小?(只能一个司机接一个乘客)算法
带权二分图方法
通常对KM算法的描述,基本上能够归纳成如下几个步骤:ide
(1) 初始化可行标杆
(2) 用匈牙利算法寻找完备匹配
(3) 若未找到完备匹配则修改可行标杆
(4) 重复(2)(3)直到找到相等子图的完备匹配spa
关于该算法的流程及实施,网上有不少介绍,基本上都是围绕可行标杆如何修改而进行的讨论,至于原理并无给出深刻的探讨。3d
KM算法是用于寻找带权二分图最佳匹配的算法。code
二分图是这样一种图:全部顶点能够分红两个集:X和Y,其中X和Y中的任意两个在同一个集中的点都不相连,而来自X集的顶点与来自Y集的顶点有连线。当这些连线被赋于必定的权重时,这样的二分图即是带权二分图。blog
二分图匹配是指求出一组边,其中的顶点分别在两个集合中,且任意两条边都没有相同的顶点,这组边叫作二分图的匹配,而所能获得的最大的边的个数,叫作二分图的最大匹配。递归
咱们也能够换个角度看二分图的最大匹配,即二分图的每条边的默认权重为1,咱们求到的二分图的最大匹配的权重最大。对于带权二分图,其边有大于0的权重,找到一组匹配,使其权重最大,即为带权二分图的最佳匹配。队列
匈牙利算法通常用于寻找二分图的最大匹配。算法根据必定的规则选择二分图的边加入匹配子图中,其基本模式为:数学
初始化匹配子图为空
While 找获得增广路径
Do 把增广路径添加到匹配子图中string
增广路径有以下特性:
例以下图,蓝色的是当前的匹配子图,目前只有边x0y0,而后经过x1找到了增广路径:x1y0->y0x0->x0y2
其中第奇数第边x1y0和x0y2不在当前的匹配子图中,而第偶数条边x0y0在匹配子图中,经过添加x1y0和x0y2到匹配子图并删除x0y0,使得匹配数由1增长到了2。每找到一条增广路径,经过添加删除边,咱们老是能使匹配数加1.
增广路径有两种寻径方法,一个是深搜,一个是宽搜。例如从x2出发寻找增广路径,若是是深搜,x2找到y0匹配,但发现y0已经被x1匹配了,因而就深刻到x1,去为x1找新的匹配节点,结果发现x1没有其余的匹配节点,因而匹配失败,x2接着找y1,发现y1能够匹配,因而就找到了新的增广路径。若是是宽搜,x1找到y0节点的时候,因为不能立刻获得一个合法的匹配,因而将它作为候选项放入队列中,并接着找y1,因为y1已经匹配,因而匹配成功返回了。相对来讲,深搜要容易理解些,其栈能够由递归过程来维护,而宽搜则须要本身维护一个队列,并对一路过来的路线本身作标记,实现起来比较麻烦。
对于带权重的二分图来讲,咱们能够把它当作一个全部X集合的顶点到全部Y集合的顶点均有边的二分图(把原来没有的边添加入二分图,权重为0便可),也就是说它一定存在完备匹配(即其匹配数为min(|X|,|Y|))。为了使权重达到最大,咱们其实是经过贪心算法来选边,造成一个新的二分图(咱们下面叫它二分子图好了),并在该二分图的基础上寻找最大匹配,当该最大匹配为完备匹配时,咱们能够肯定该匹配为最佳匹配。(在这里咱们如此定义最大匹配:匹配边数最多的匹配和最佳匹配:匹配边的权重和最大的匹配。)
贪心算法老是将最优的边优先加入二分子图,该最优的边将对当前的匹配子图带来最大的贡献,贡献的衡量是经过标杆来实现的。下面咱们将经过一个实例来解释这个过程。
有带权二分图:
算法把权重转换成标杆,X集跟Y集的每一个顶点各有一个标杆值,初始状况下权重所有放在X集上。因为每一个顶点都将至少会有一个匹配点,贪心算法必然优先选择该顶点上权重最大的边(最理想的状况下,这些边正好没有交点,因而咱们天然获得了最佳匹配)。最初的二分子图为:(能够看到初始化时X标杆为该顶点上的最大权重,而Y标杆为0)
从X0找增广路径,找到X0Y4;从X1找不到增广路径,也就是说,必须往二分子图里边添加新的边,使得X1能找到它的匹配,同时使权重总和添加最大。因为X1通往Y4而Y4已经被X0匹配,因此有两种可能,一个是为X0找一个新的匹配点并把Y4让给X1,或者是为X1找一个新的匹配点,如今咱们将要看到标杆的做用了。根据传统的算法描述,可以进入二分子图的边的条件为L(x)+L(y)>=weight(xy)。当找不到增广路径时,对于搜索过的路径上的XY点,设该路径上的X顶点集为S,Y顶点集为T,对全部在S中的点xi及不在T中的点yj,计算d=min{(L(xi)+L(yj)-weight(xiyj))},从S集中的X标杆中减去d,并将其加入到T集中的Y的标杆中,因为S集中的X标杆减小了,而不在T中的Y标杆不变,至关于这两个集合中的L(x)+L(y)变小了,也就是,有新的边能够加入二分子图了。从贪心选边的角度看,咱们能够为X0选择新的边而抛弃原先的二分子图中的匹配边,也能够为X1选择新的边而抛弃原先的二分子图中的匹配边,由于咱们不能同时选择X0Y4和X1Y4,由于这是一个不合法匹配,这个时候,d=min{(L(xi)+L(yj)-weight(xiyj))}的意义就在于,咱们选择一条新的边,这条边将被加入匹配子图中使得匹配合法,选择这条边造成的匹配子图,将比原先的匹配子图加上这条非法边组成的非法匹配子图的权重和(若是它是合法的,它将是最大的)小最少,即权重最大了。好绕口的。用数学的方式表达,设原先的不合法匹配(它的权重最大,由于咱们老是从权重最大的边找起的)的权重为W,新的合法匹配为W’,d为min{W-W’i}。在这个例子中,S={X0, X1},Y={Y4},求出最小值d=L(X1)+L(Y0)-weight(X1Y0)=2,获得新的二分子图:
从新为X1寻找增广路径,找到X1Y0,能够看到新的匹配子图的权重为9+6=15,比原先的不合法的匹配的权重9+8=17正好少d=2。
接下来从X2出发找不到增广路径,其走过的路径如蓝色的路线所示。造成的非法匹配子图:X0Y4,X1Y0及X2Y0的权重和为22。在这条路径上,只要为S={X0,X1,X2}中的任意一个顶点找到新的匹配,就能够解决这个问题,因而又开始求d。
d=L(X0)+L(Y2)-weight(X0Y2)=L(X2)+L(Y1)-weight(X2Y1)=1.
新的二分子图为:
从新为X2寻找增广路径,若是咱们使用的是深搜,会获得路径:X2Y0->Y0X1->X1Y4->Y4X0->X0Y2,即奇数条边而删除偶数条边,新的匹配子图中由这几个顶点获得的新的权重为21;若是使用的是宽搜,会获得路径X2Y1,另上原先的两条匹配边,权重为21。假设咱们使用的是宽搜,获得的新的匹配子图为:
接下来依次类推,直到为X4找到一个匹配点。
KM算法的最大特色在于利用标杆和权重来生成一个二分子图,在该二分子图上面找最大匹配,并且,当些仅当找到完备匹配,才能获得最佳匹配。标杆和权重的做用在于限制新边的加入,使得加入的新边老是能为子图添加匹配数,同时又令权重和获得最大的提升。
#include <cstdio> #include <string.h> #include <vector> #include <algorithm> using namespace std; int const MAX = 1000; int const inf = 0x3fffffff; int w[MAX][MAX]; int link[MAX];//表明当前与Y集合中配对的X集合中的点 int visx[MAX], visy[MAX]; int lx[MAX], ly[MAX]; int n, m;//表明X和Y中元素的个数 int can(int t) { visx[t] = 1; for(int i = 1; i <= m; i++){ if(!visy[i] && lx[t] + ly[i] == w[t][i]){//这里“lx[t]+ly[i]==w[t][i]”决定了这是在相等子图中找增广路的前提,很是重要 visy[i] = 1; if(link[i] == -1 || can(link[i])){ link[i] = t; return 1; } } } return 0; } int km() { int sum = 0; memset(ly, 0, sizeof(ly)); for(int i = 1; i <= n; i++){//把各个lx的值都设为当前w[i][j]的最大值 lx[i] = -inf; for(int j = 1; j <= n; j++){ if(lx[i] < w[i][j]) lx[i] = w[i][j]; } } memset(link, -1, sizeof(link)); for(int i = 1; i <= n; i++){ while(1){ memset(visx, 0, sizeof(visx)); memset(visy, 0, sizeof(visy)); if(can(i))//若是它可以造成一条增广路径,那么就break break; int d = inf;//不然,后面应该加入新的边,这里应该先计算d值 for(int j = 1; j <= n; j++)//对于搜索过的路径上的XY点,设该路径上的X顶点集为S,Y顶点集为T,对全部在S中的点xi及不在T中的点yj if(visx[j]) for(int k = 1; k <= m; k++) if(!visy[k]) d = min(d, lx[j] + ly[k] - w[j][k]); if(d == inf) return -1;//找不到能够加入的边,返回失败(即找不到完美匹配) for (int j = 1; j <= n; j++) if (visx[j]) lx[j] -= d; for(int j = 1; j <= m; j++) if(visy[j]) ly[j] += d; } } for(int i = 1; i <= m; i++) if(link[i] > -1) sum += w[link[i]][i]; return sum; }
这个地方注意 咱们是求最小值 只须要把上面的换个符号便可 上面的算法是求最大值