转自:http://blog.163.com/huangbingliang@yeah/blog/static/94161399201011291044527/
参考 http://web.nuist.edu.cn/courses/lssx/longtime/part4/chapter14/14_01_10_01.htm
定义14.8 设G=<V,E>,G'=<V',E'>为两个图(同为无向图或同为有向图),若V'V且E'
E,则称G'是G的子图,G为G'的母图,记作G'
G. 又若V'
V或E'
E,则称G'为G的真子图。若V'=V,则称G'为G的生成子图。
设G=<V,E>为一图,V1V且V1≠
,称以V1为顶点集,以G中两个端点都在V1中的边组成边集E1的图为G的V1导出的子图,记作G[V1]. 又设E1
E且E1≠
,称以E1为边集,以E1中边关联的顶点为顶点集V1的图为G的E1导出的子图,记作G[E1].
在图14.5中,设G为(1)中图所表示,取V1={a,b,c},则V1的导出子图G[V1]为(2)中图所示。取E1={e1,e3},则E1的导出子图G[E1]为(3)中图所示。
图14.5
对于给定的正整数n和m(m≤n(n-1)/2),构造出所有非同构的n阶m条边的所有非同构的无向(有向)简单图,这是目前还没有解决的难题,但对于比较小的n还是能构造出来的。
完备匹配
参考http://222.22.224.75/lssx/part4/chapter18/18_03_01_01.htm
定义18.7 设G=<V1,V2,E>为二部图,|V1|≤|V2|,M为G中一个最大匹配,且|M|=|V1|,则称M为V1到V2的完备匹配。
在上述定义中,若|V2|=|V1|,则完备匹配即为完美匹配,若|V1|<|V2|,则完备匹配为G中最大匹配。
图18.4中(1),(2)中都存在完备匹配(见绿线边所示),而(3)中没有完备匹配,绿线边所示的边组成的集合是最大匹配,但不是完备匹配。一个二部图是否存在完备匹配,已经有了判别法。即 hall定理
Hall定理:设二分图G=<V1,V2,E>,|V1|<=|V2|,G从V1到V2有完备匹配当且仅当V1中任意k个顶点至少与V2中k个顶点相邻。
参考http://wenku.baidu.com/view/28344341be1e650e52ea9932.html
KM算法是通过给每个顶点一个标号(叫做顶标)来把求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B[i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j),A[i]+B[j]>=w[i,j]始终成立,初始A[i]为与xi相连的边的最大边权,B[j]=0。KM算法的正确性基于以下定理:
若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。
因为对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和(即不是最优匹配)。所以相等子图的完备匹配一定是二分图的最大权匹配。
Important:
1 如果图中|V1|!=|V2|,理解的时候可以补充一些虚拟的点,让两边的点数相等(完备匹配就成为完美匹配了)。当然虚拟点的边之都是0。
2 为什么可以保证相等子图就一定有完备匹配呢?关键就是标号的不断变化可以使得边权为0的边(其实边权为0就不算是边了,但我们可以想像为虚拟的边)也加入到相等子图中,因为完全可能存在A[i]=0,b[j]=0 。最后可以发现凡是原图无法提供的边都是由这类虚拟边来代替的。
最简单的例子就是一个没有边的二分图,最优匹配当然就是0了,其相等子图其实是个完全的二分图,每一个V1的顶点都有一条边权为0的边到达V2的所有顶点,而其相等子图的完备匹配当然就是由一些为0的边组成的,而且所有点的标号也都是0. 所以无论原图中的边够不够,相等子图都可以找出一个完备匹配。
由上面两条分析可以看出来,相等子图的作用其实不在于找完备匹配,而是在在完备匹配的过程中对标号的修改,修改保证完备匹配一定可以找到,而标号本身则是证明km算法正确性的关键。
基于上述两点进行扩充,所有上面的定理也就很好理解了。
理解:相等子图包含原图的所有的点,相等子图一定可以找到完备匹配,相等子图的完备匹配只需加一些虚拟点可以扩充为完美匹配(记为M),完美匹配是包含了所有点的匹配,那么所有点的顶点的标号值都包括进来了,虽然有些点是0,在这个状态下,把相等子图的标号一一对应的标到原图上去,原图的任意一个匹配最多只能包含原图的所有顶点,即任何匹配的权和不可能超过所有标号的和,所以M的和必然是最优的
证明完之后看看实现和代码:
朴素的实现方法,时间复杂度为O(n4)——需要找O(n)次增广路, 每次增广最多需要修改O(n)次顶 标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n2)。实际上KM算法的复杂度是可以做到O(n3)的。我们给每个Y顶点一个“松弛量”函数 slack,每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边(i,j)时,如果它不在相等子图中,则让slack[j]变成原值与A [i]+B[j]-w[i,j]的较小值。这样,在修改顶标时,取所有不在交错树中的Y顶点的slack值中的最小值作为d值即可。但还要注意一点:修改 顶标后,要把所有的slack值都减去d。
算法流程:
(1)初始化可行顶标的值
将V1的点的标号记为与其相连边的最大边权值,V2的点标号全记为0
(3)若未找到完备匹配则修改可行顶标的值 ,扩充相等子图
(4)重复(2)(3)直到找到相等子图的完备匹配为止
代码:
#include <iostream>
#include <cstdio>
#include <memory.h>
#include <algorithm>
using namespace std;
#define MAX 100
int n;
int weight[MAX][MAX]; //权重
int lx[MAX],ly[MAX]; //定点标号
bool sx[MAX],sy[MAX]; //记录寻找增广路时点集x,y里的点是否搜索过
int match[MAX]; //match[i]记录y[i]与x[match[i]]相对应
bool search_path(int u) { //给x[u]找匹配,这个过程和匈牙利匹配是一样的
sx[u]=true;
for(int v=0; v<n; v++){
if(!sy[v] &&lx[u]+ly[v] == weight[u][v]){
sy[v]=true;
if(match[v]==-1 || search_path(match[v])){
match[v]=u;
return true;
}
}
}
return false;
}
int Kuhn_Munkras(bool max_weight){
if(!max_weight){ //如果求最小匹配,则要将边权取反
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
weight[i][j]=-weight[i][j];
}
//初始化顶标,lx[i]设置为max(weight[i][j] | j=0,..,n-1 ), ly[i]=0;
for(int i=0;i<n;i++){
ly[i]=0;
lx[i]=-0x7fffffff;
for(int j=0;j<n;j++)
if(lx[i]<weight[i][j])
lx[i]=weight[i][j];
}
memset(match,-1,sizeof(match));
//不断修改顶标,直到找到完备匹配或完美匹配
for(int u=0;u<n;u++){ //为x里的每一个点找匹配
while(1){
memset(sx,0,sizeof(sx));
memset(sy,0,sizeof(sy));
if(search_path(u)) //x[u]在相等子图找到了匹配,继续为下一个点找匹配
break;
//如果在相等子图里没有找到匹配,就修改顶标,直到找到匹配为止
//首先找到修改顶标时的增量inc, min(lx[i] + ly [i] - weight[i][j],inc);,lx[i]为搜索过的点,ly[i]是未搜索过的点,因为现在是要给u找匹配,所以只需要修改找的过程中搜索过的点,增加有可能对u有帮助的边
int inc=0x7fffffff;
for(int i=0;i<n;i++)
if(sx[i])
for(int j=0;j<n;j++)
if(!sy[j]&&((lx[i] + ly [j] - weight[i][j] )<inc))
inc = lx[i] + ly [j] - weight[i][j] ;
//找到增量后修改顶标,因为sx[i]与sy[j]都为真,则必然符合lx[i] + ly [j] =weight[i][j],然后将lx[i]减inc,ly[j]加inc不会改变等式,但是原来lx[i] + ly [j] !=weight[i][j]即sx[i]与sy[j]最多一个为真,lx[i] + ly [j] 就会发生改变,从而符合等式,边也就加入到相等子图中
if(inc==0) cout<<"fuck!"<<endl;
for(int i=0;i<n;i++){
if(sx[i]) //
lx[i]-=inc;
if(sy[i])
ly[i]+=inc;
}
}
}
int sum=0;
for(int i=0;i<n;i++)
if(match[i]>=0)
sum+=weight[match[i]][i];
if(!max_weight)
sum=-sum;
return sum;
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&weight[i][j]);
printf("%d\n",Kuhn_Munkras(1));
system("pause");
return 0;
}