博客推荐:KM算法详解 我对KM算法的理解 二分图的最佳完美匹配--KM算法
1、KM算法是用于寻找带权二分图最佳匹配的算法。 对KM算法的描述,基本上可以概括成以下几个步骤:
(1) 初始化可行标杆
(2) 用匈牙利算法寻找完备匹配
(3) 若未找到完备匹配则修改可行标杆
(4) 重复(2)(3)直到找到相等子图的完备匹配
2、二分图详解:
(1)二分图定义:
所有顶点可以分成两个集:X和Y,其中X和Y中的任意两个在同一个集中的点都不相连,而来自X集的顶点与来自Y集的顶点有连线。当这些连线被赋于一定的权重时,这样的二分图便是带权二分图。
(2)二分图判定:
无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。判断无向连通图是不是二分图,可以使用深度优先遍历算法(又名交叉染色法)。下面着重介绍下交叉染色法的定义与原理
首先任意取出一个顶点进行染色,和该节点相邻的点有三种情况:
a.如果节点没有染过色,就染上与它相反的颜色,推入队列,
b.如果节点染过色且相反,忽视掉,
c.如果节点染过色且与父节点相同,证明不是二分图,return
(3)二分图匹配:
二分图匹配是指求出一组边,其中的顶点分别在两个集合中,且任意两条边都没有相同的顶点,这组边叫做二分图的匹配,而所能得到的最大的边的个数,叫做二分图的最大匹配。 我们也可以换个角度看二分图的最大匹配,即二分图的每条边的默认权重为1,我们求到的二分图的最大匹配的权重最大。对于带权二分图,其边有大于0的权重,找到一组匹配,使其权重最大,即为带权二分图的最佳匹配。
3、增广路径:
(1)增广路径定义比较晦涩难懂,直接看它的特性更清晰一些,增广路径有如下特性:
- a. 有奇数条边
- b. 起点在二分图的X边,终点在二分图的Y边
- c. 路径上的点一定是一个在X边,一个在Y边,交错出现。
- d. 整条路径上没有重复的点
- e. 起点和终点都是目前还没有配对的点,其他的点都已经出现在匹配子图中
- f. 路径上的所有第奇数条边都是目前还没有进入目前的匹配子图的边,而所有第偶数条边都已经进入目前的匹配子图。奇数边比偶数边多一条边
- g. 于是当我们把所有第奇数条边都加到匹配子图并把条偶数条边都删除,匹配数增加了1.
(2)找增广路径的过程:目前图中只有(X0, Y0)匹配成功,也就是说蓝色这条线是已匹配子图(目前只有一条边),现在X1想要配对,只能和Y0进行配对,但发现Y0已经被X0匹配了,于是就深入到X0,去为X0找新的匹配节点,发现X0可以和Y2匹配,而且Y2处于未匹配状态,因此通过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已经匹配,于是匹配成功返回了。
4、匈牙利算法:
匈牙利算法一般用于寻找二分图的最大匹配。算法根据一定的规则选择二分图的边加入匹配子图中,其基本模式为:
初始化匹配子图为空
While 找得到增广路径
Do 把增广路径添加到匹配子图中
匈牙利算法就是在不断寻找增广路,如果找不到增广路,就说明达到了最大匹配。
匈牙利算法代码:
//---------------------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;
}
5、KM算法:
(1)KM算法,用于求二分图匹配的最佳匹配。何为最佳匹配?就是带权二分图的权值最大的完备匹配称为最佳匹配。 那么何为完备匹配?X部中的每一个顶点都与Y部中的一个顶点匹配,或者Y部中的每一个顶点也与X部中的一个顶点匹配,则该匹配为完备匹配。
KM算法的最大特点在于利用标杆和权重来生成一个二分子图,在该二分子图上面找最大匹配,而且,当些仅当找到完备匹配,才能得到最佳匹配。标杆和权重的作用在于限制新边的加入,使得加入的新边总是能为子图添加匹配数,同时又令权重和得到最大的提高。
(2)KM算法步骤:
- a.用邻接矩阵(或其他方法也行啦)来储存图,注意:如果只是想求最大权值匹配而不要求是完全匹配的话,请把各个不相连的边的权值设置为0。
- b.运用贪心算法初始化标杆。
- c.运用匈牙利算法找到完备匹配。
- d.如果找不到,则通过修改标杆,增加一些边。
- e.重复c,d的步骤,直到完全匹配时可结束。
(3)KM算法顶标
二分图最佳匹配还是二分图匹配,所以跟和匈牙利算法思路差不多。二分图是特殊的网络流,最佳匹配相当于求最大(小)费用最大流,所以FF算法(全名Ford-Fulkerson算法)也能实现。
- 所以我们可以把这匈牙利算法和FF算法结合起来。这就是KM算法的思路了:尽量找最大的边进行连边,如果不能则换一条较大的。
- FF算法里面,我们每次是找最长(短)路进行通流,所以二分图匹配里面我们也按照FF算法找最大边进行连边!
但是遇到某个点被匹配了两次怎么办?那就用匈牙利算法进行更改匹配!所以,根据KM算法的思路,我们一开始要对边权值最大的进行连线。那问题就来了,我们如何让计算机知道该点对应的权值最大的边是哪一条?或许我们可以通过某种方式记录边的另一端点,但是呢,后面还要涉及改边,又要记录边权值总和,而这个记录端点方法似乎有点麻烦。于是KM采用了一种十分巧妙的办法(也是KM算法思想的精髓):添加标杆(顶标)
添加标杆(顶标)流程:
- 我们对左边每个点Xi和右边每个点Yi添加标杆Cx和Cy。其中我们要满足Cx+Cy>=w[x][y](w[x][y]即为点Xi、Yi之间的边权值)
- 对于一开始的初始化,我们对于每个点分别进行如下操作:Cx=max(w[x][y]); Cy=0;
添加顶标之前的二分图:
添加顶标之后的二分图: