我对KM算法的理解

一般对KM算法的描述,基本上可以概括成以下几个步骤:
(1) 初始化可行标杆
(2) 用匈牙利算法寻找完备匹配
(3) 若未找到完备匹配则修改可行标杆
(4) 重复(2)(3)直到找到相等子图的完备匹配

关于该算法的流程及实施,网上有很多介绍,基本上都是围绕可行标杆如何修改而进行的讨论,至于原理并没有给出深入的探讨。

KM算法是用于寻找带权二分图最佳匹配的算法。

二分图是这样一种图:所有顶点可以分成两个集:X和Y,其中X和Y中的任意两个在同一个集中的点都不相连,而来自X集的顶点与来自Y集的顶点有连线。当这些连线被赋于一定的权重时,这样的二分图便是带权二分图。

二分图匹配是指求出一组边,其中的顶点分别在两个集合中,且任意两条边都没有相同的顶点,这组边叫做二分图的匹配,而所能得到的最大的边的个数,叫做二分图的最大匹配。

我们也可以换个角度看二分图的最大匹配,即二分图的每条边的默认权重为1,我们求到的二分图的最大匹配的权重最大。对于带权二分图,其边有大于0的权重,找到一组匹配,使其权重最大,即为带权二分图的最佳匹配。

匈牙利算法一般用于寻找二分图的最大匹配。算法根据一定的规则选择二分图的边加入匹配子图中,其基本模式为:

初始化匹配子图为空
While 找得到增广路径
Do 把增广路径添加到匹配子图中

增广路径有如下特性:
1. 有奇数条边
2. 起点在二分图的X边,终点在二分图的Y边
3. 路径上的点一定是一个在X边,一个在Y边,交错出现。
4. 整条路径上没有重复的点
5. 起点和终点都是目前还没有配对的点,其他的点都已经出现在匹配子图中
6. 路径上的所有第奇数条边都是目前还没有进入目前的匹配子图的边,而所有第偶数条边都已经进入目前的匹配子图。奇数边比偶数边多一条边
7. 于是当我们把所有第奇数条边都加到匹配子图并把条偶数条边都删除,匹配数增加了1.

例如下图,蓝色的是当前的匹配子图,目前只有边x0y0,然后通过x1找到了增广路径:x1y0->y0x0->x0y2

[img]http://dl.iteye.com/upload/attachment/0078/4288/29a781cc-4459-3cc6-8f8d-6f5d0584424f.jpg[/img]

其中第奇数第边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|))。为了使权重达到最大,我们实际上是通过贪心算法来选边,形成一个新的二分图(我们下面叫它二分子图好了),并在该二分图的基础上寻找最大匹配,当该最大匹配为完备匹配时,我们可以确定该匹配为最佳匹配。(在这里我们如此定义最大匹配:匹配边数最多的匹配和最佳匹配:匹配边的权重和最大的匹配。)

贪心算法总是将最优的边优先加入二分子图,该最优的边将对当前的匹配子图带来最大的贡献,贡献的衡量是通过标杆来实现的。下面我们将通过一个实例来解释这个过程。

有带权二分图:

[img]http://dl.iteye.com/upload/attachment/0078/4290/07a9b14e-682f-344a-856d-3c2531e10683.jpg[/img]
算法把权重转换成标杆,X集跟Y集的每个顶点各有一个标杆值,初始情况下权重全部放在X集上。由于每个顶点都将至少会有一个匹配点,贪心算法必然优先选择该顶点上权重最大的边(最理想的情况下,这些边正好没有交点,于是我们自然得到了最佳匹配)。最初的二分子图为:(可以看到初始化时X标杆为该顶点上的最大权重,而Y标杆为0)

[img]http://dl.iteye.com/upload/attachment/0078/4292/8364bc08-dd7a-3964-bb9d-9f022fccc200.jpg[/img]
从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,得到新的二分子图:

[img]http://dl.iteye.com/upload/attachment/0078/4294/73d1c966-bc50-30d3-89da-8c6a97994d92.png[/img]
重新为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.
新的二分子图为:

[img]http://dl.iteye.com/upload/attachment/0078/4302/9126bc13-4660-377f-b65d-1cc70c836695.png[/img]

重新为X2寻找增广路径,如果我们使用的是深搜,会得到路径:X2Y0->Y0X1->X1Y4->Y4X0->X0Y2,即奇数条边而删除偶数条边,新的匹配子图中由这几个顶点得到的新的权重为21;如果使用的是宽搜,会得到路径X2Y1,另上原先的两条匹配边,权重为21。假设我们使用的是宽搜,得到的新的匹配子图为:

[img]http://dl.iteye.com/upload/attachment/0078/4296/9e0b0a27-a040-31e7-8a11-ab94265fb58c.png[/img]
接下来依次类推,直到为X4找到一个匹配点。

KM算法的最大特点在于利用标杆和权重来生成一个二分子图,在该二分子图上面找最大匹配,而且,当些仅当找到完备匹配,才能得到最佳匹配。标杆和权重的作用在于限制新边的加入,使得加入的新边总是能为子图添加匹配数,同时又令权重和得到最大的提高。

下面是匈牙利算法的dfs和bfs实现,是用c++实现的:

//---------------------DFS---------------------------------
#include<iostream>
#include<memory.h>
using namespace std;

#define MAXN 10
int graph[MAXN][MAXN];
int match[MAXN];
int visitX[MAXN], visitY[MAXN];
int nx, ny;

bool findPath( int u )
{
visitX[u] = 1;
for( int v=0; v<ny; v++ )
{
if( !visitY[v] && graph[u][v] )
{
visitY[v] = 1;
if( match[v] == -1 || findPath(match[v]) )
{
match[v] = u;
return true;
}
}
}
return false;
}

int dfsHungarian()
{
int res = 0;
memset( match, -1, sizeof(match) );
for( int i=0; i<nx; i++ )
{
memset( visitX, 0, sizeof(visitX) );
memset( visitY, 0, sizeof(visitY) );
if( findPath(i) )
res++;
}
return res;
}

//-----------------------------BFS-------------------------------
#include<iostream>
#include<memory.h>
using namespace std;

#define MAXN 10
int graph[MAXN][MAXN];
//在bfs中,增广路径的搜索是一层一层展开的,所以必须通过prevX来记录上一层的顶点
//chkY用于标记某个Y顶点是否被目前的X顶点访问尝试过。
int matchX[MAXN], matchY[MAXN], prevX[MAXN], chkY[MAXN];
int queue[MAXN];
int nx, ny;

int bfsHungarian()
{
int res = 0;
int qs, qe;
memset( matchX, -1, sizeof(matchX) );
memset( matchY, -1, sizeof(matchY) );
memset( chkY, -1, sizeof(chkY) );

for( int i=0; i<nx; i++ )
{
if( matchX[i] == -1 ) //如果该X顶点未找到匹配点,将其放入队列。
{
qs = qe = 0;
queue[qe++] = i;
prevX[i] = -1; //并且标记,它是路径的起点
bool flag = 0;

while( qs<qe && !flag )
{
int u = queue[qs];
for( int v=0; v<ny&&!flag; v++ )
{
if( graph[u][v] && chkY[v]!=i ) //如果该节点与u有边且未被访问过
{
chkY[v] = i; //标记且将它的前一个顶点放入队列中,也就是下次可能尝试这个顶点看能否为它找到新的节点
queue[qe++] = matchY[v];
if( matchY[v] >= 0 )
prevX[matchY[v]] = u;
else //到达了增广路径的最后一站
{
flag = 1;
int d=u, e=v;
while( d!=-1 ) //一路通过prevX找到路径的起点
{
int t = matchX[d];
matchX[d] = e;
matchY[e] = d;
d = prevX[d];
e = t;
}
}
}
}
qs++;
}
if( matchX[i] != -1 )
res++;
}
}
return res;
}


最优匹配算法因为是项目需要,我用的是java。

public class KuhnMunkres {

private int maxN, n, lenX, lenY;
private double[][] weights;
private boolean[] visitX, visitY;
private double[] lx, ly;
private double[] slack;
private int[] match;

public KuhnMunkres( int maxN )
{
this.maxN = maxN;
visitX = new boolean[maxN];
visitY = new boolean[maxN];
lx = new double[maxN];
ly = new double[maxN];
slack = new double[maxN];
match = new int[maxN];
}

public int[][] getMaxBipartie( double weight[][], double[] result )
{
if( !preProcess(weight) )
{
result[0] = 0.0;
return null;
}
//initialize memo data for class
//initialize label X and Y
Arrays.fill(ly, 0);
Arrays.fill(lx, 0);
for( int i=0; i<n; i++ )
{
for( int j=0; j<n; j++ )
{
if( lx[i]<weights[i][j])
lx[i] = weights[i][j];
}
}

//find a match for each X point
for( int u=0; u<n; u++ )
{
Arrays.fill(slack, 0x7fffffff);
while(true)
{
Arrays.fill(visitX, false);
Arrays.fill(visitY, false);
if( findPath(u) ) //if find it, go on to the next point
break;
//otherwise update labels so that more edge will be added in
double inc = 0x7fffffff;
for( int v=0; v<n; v++ )
{
if( !visitY[v] && slack[v] < inc )
inc = slack[v];
}
for( int i=0; i<n; i++ )
{
if( visitX[i] )
lx[i] -= inc;
if( visitY[i] )
ly[i] += inc;
}
}
}
result[0] = 0.0;
for( int i=0; i<n; i++ )
{
if( match[i] >= 0 )
result[0] += weights[match[i]][i];
}
return matchResult();
}

public int[][] matchResult()
{
int len = Math.min(lenX, lenY);
int[][] res = new int[len][2];
int count=0;
for( int i=0; i<lenY; i++ )
{
if( match[i] >=0 && match[i]<lenX )
{
res[count][0] = match[i];
res[count++][1] = i;
}
}
return res;
}

private boolean preProcess( double[][] weight )
{
if( weight == null )
return false;
lenX = weight.length; lenY = weight[0].length;
if( lenX>maxN || lenY>maxN )
return false;
Arrays.fill(match, -1);
n = Math.max(lenX, lenY);
weights = new double[n][n];
for( int i=0; i<n; i++ )
Arrays.fill(weights[i], 0.0);
for( int i=0; i<lenX; i++ )
for( int j=0; j<lenY; j++ )
weights[i][j] = weight[i][j];
return true;
}

private boolean findPath( int u )
{
visitX[u] = true;
for( int v=0; v<n; v++ )
{
if( !visitY[v] )
{
double temp = lx[u]+ly[v]-weights[u][v];
if( temp == 0.0 )
{
visitY[v] = true;
if( match[v] == -1 || findPath(match[v]) )
{
match[v] = u;
return true;
}
}
else
slack[v] = Math.min(slack[v], temp);
}
}
return false;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值