KM算法模板

本文深入解析生成子图的概念及其在图论中的应用,详细讲解了生成子图的两种导出方式,并通过实例展示了如何利用生成子图解决实际问题。此外,文章还介绍了完备匹配的概念及判定方法,特别是Hall定理在判定完备匹配中的作用,并详细阐述了KM算法的核心思想、实现步骤以及其实现代码,旨在为读者提供全面深入的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自: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'生成子图和完备匹配的概念 - huangbingliang@yeah - evagleV且E'生成子图和完备匹配的概念 - huangbingliang@yeah - evagleE,则称G'是G的子图,G为G'的母图,记作G'生成子图和完备匹配的概念 - huangbingliang@yeah - evagleG. 又若V'生成子图和完备匹配的概念 - huangbingliang@yeah - evagleV或E'生成子图和完备匹配的概念 - huangbingliang@yeah - evagleE,则称G'为G的真子图。若V'=V,则称G'为G的生成子图

  设G=<V,E>为一图,V1生成子图和完备匹配的概念 - huangbingliang@yeah - evagleV且V1生成子图和完备匹配的概念 - huangbingliang@yeah - evagle,称以V1为顶点集,以G中两个端点都在V1中的边组成边集E1的图为G的V1导出的子图,记作G[V1]. 又设E1生成子图和完备匹配的概念 - huangbingliang@yeah - evagleE且E1生成子图和完备匹配的概念 - huangbingliang@yeah - evagle,称以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)中图所示。

生成子图和完备匹配的概念 - huangbingliang@yeah - evagle

图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定理

生成子图和完备匹配的概念 - huangbingliang@yeah - evagle


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算法的正确性基于以下定理:

设 G(V,E) 为二部图, G'(V,E') 为二部图的子图。如果对于 G' 中的任何边<x,y> 满足, L(x)+ L(y)== Wx,y,我们称 G'(V,E') 为 G(V,E) 的等价子图或相等子图(是G的生成子图)。

若由二分图中所有满足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

  (2)用匈牙利算法在相等子图寻找完备匹配   
       (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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值