关于KM算法及其延伸的一些解题研究

本文深入探讨KM算法,一种高效解决带权二分图匹配问题的算法,详细讲解其核心思想与实现步骤,并通过具体案例展示算法的应用场景。

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

关于KM算法及其延伸的一些解题研究

KM算法作为一种简洁明了的算法,巧妙地解决了带权二分图在特定条件(带权最大匹配一定是完备匹配的图)下的求解问题,规避了网络流费用流算法较为繁琐的代码,在信息学竞赛具有相当的地位。为增加本文的可读性,笔者已经将输入输出和初始化的代码隐去。

题目描述

仓库管理员M最近一直很烦恼,因为他的上司给了他一个艰难的任务:让他尽快想出一种合理的方案,把公司的仓库整理好。已知公司共有n个仓库和n种货物,由于公司进货时没能很好的归好类,使得大部分的仓库里面同时装有多种货物,这就给搬运工作人员搬运货物时带来了很多的麻烦。仓库管理员M的任务就是设计一种合理的方案,把仓库里面的货物重新整理,把相同的货物放到同一个仓库,以便于日后的管理,在整理过程中肯定需要把某些货物从一个仓库搬运到另一个仓库,已知每一次搬运货物所付出的代价等于搬运该货物的重量。

编程任务:

请你帮助仓库管理员M设计搬运方案,使得把所有的货物归好类:使每种货物各自占用一个仓库,或者说每个仓库里只能放一种货物。同时要求搬运货物时所付出的所有的总的代价最小。

输入格式

第一行为n (1 <= n <= 150),仓库的数量。

以下为仓库货物的情况。第i+1行依次为第i个仓库中n种货物的数量x(0 <= x <= 100)。

输出格式

把所有的货物按要求整理好所需的总的最小代价。

输入输出样例

输入 

4

62 41 86 94

73 58 11 12

69 93 89 88

81 40 69 13

输出 

650

说明/提示

样例说明:方案是:第1种货物放到仓库2中;第2种货物放到仓库3中;第3种货物放到仓库4中;第4种货物放到仓库1中

首先给出笔者解题代码。定义等略去。

bool dfs(int x)

{

    va[x]=1;

    for(int i=1;i<=n;i++)

    {

        if(!vb[i])

        {

            if((la[x]+lb[i])==w[x][i])

            {

                vb[i]=1;

                if(!match[i]||dfs(match[i]))

                {

                    match[i]=x;

                    return true;

                }

            }

            else delta=min(delta,la[x]+lb[i]-w[x][i]);

        }

    }

    return false;

}

int KM()

{

    for(int i=1;i<=n;i++)

    {

        la[i]=-(1<<30);

        lb[i]=0;

        for(int j=1;j<=n;j++)

        {

            la[i]=max(la[i],w[i][j]);

        }

    }

    for(int i=1;i<=n;i++)

    {

        while(true)

        {

            memset(va,0,sizeof(va));

            memset(vb,0,sizeof(vb));

            delta=(1<<30);

            if(dfs(i))break;

            for(int j=1;j<=n;j++)

            {

                if(va[j])la[j]-=delta;

                if(vb[j])lb[j]+=delta;

            }

        }

    }

    int ans=0;

    for(int i=1;i<=n;i++)

    {

        ans-=w[match[i]][i];

    }

    return ans;

}

int main()

{

    n=read();

    for(int i=1;i<=n;i++)

        for(int j=1;j<=n;j++)

        {

            store[i][j]=read();

            ll[j]+=store[i][j];

        }

    for(int i=1;i<=n;i++)

        for(int j=1;j<=n;j++)

        {

            w[i][j]=-(ll[j]-store[i][j]);

        }   

    cout<<KM();

    return 0;

}

本题几乎可称为KM算法的标准模板,为了进一步介绍及讨论首先引入交错树的概念(默认读者已掌握匈牙利算法的操作方式以及相关术语)

其实也很好理解,就是在一般的匈牙利算法中,从一个左部点出发尝试进行匹配,如果失败,那他所有访问过的节点以及为了访问这些节点所经过的边共同构成交错树。(该树根节点为左部点,叶子也是左部点)

之后是顶标,这个概念为相等子图而服务。 所谓相等子图,就是二分图中所有节点满足Ai+Bi=W(i,j)的边所构成的子图。 而顶标,就是这里的Ai,Bi。实际操作时,将左部点的顶标赋值为Ai,右部点赋值为Bi。

之后我们就可以引入定理

“若相等子图中存在完备匹配,则这个完备匹配就是二分图的带权最大匹配。”

根据以上定理我们可以推知,我们要做的就是采取一个适当的策略扩大相等子图的规模,直到它存在完备匹配。

for(int i=1;i<=n;i++)

    {

        la[i]=-(1<<30);

        lb[i]=0;

        for(int j=1;j<=n;j++)

        {

            la[i]=max(la[i],w[i][j]);

        }

    }

在这里我们将所有的Ai赋值为其所连边中权值最大者,Bi赋值为0。此时他必然满足满足Ai+Bj>=W(i,j)。(事实上,也存在其他赋值方法,本质上来讲,只需满足对于任意i,j有Ai+Bj>=W(I,j)即可,这里采用的是较为简便的赋值方法)

for(int i=1;i<=n;i++)

    {

        while(true)

        {

            memset(va,0,sizeof(va));

            memset(vb,0,sizeof(vb));

            delta=(1<<30);

            if(dfs(i))break;

            for(int j=1;j<=n;j++)

            {

                if(va[j])la[j]-=delta;

                if(vb[j])lb[j]+=delta;

            }

        }

    }

我们对每一个左部节点进行匹配(使用while(true)不达目的不罢休)如果dfs(i)==1,即这个节点可以匹配,那就不管他。否则修改顶标(失败的交错树中的顶标)。 这里有一点需要注意,即修改顶标后,再一次进行dfs时,原来并非相等子图中的边,现在就有可能成为相等子图中的边。而且由于减去的delta最终求得结果为“即将加入的边还比要求多多少”故可保证每次均有>=1条边进入相等子图,从概率的角度来讲,进一步提高了左部点完成匹配的可能。

以上便是该算法的核心思路,重复上述过程直到达成条件即可(所有左部点均匹配成功)。

结语:KM算法在处理最大带权二分图的特殊情况时有一定的优越性,当同样也具有较大的局限性,掌握KM算法后,一定不能满足于只解决特定情况下的问题,也还要积极探索更为高级的算法,解决更为普适性的问题。事实上,理解了二分图惯用的匈牙利算法和KM算法,对费用流的理解也会有所帮助。

感谢洛谷平台提供的题面及测试数据。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值