匈牙利算法KM算法

Posted shuizhidao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了匈牙利算法KM算法相关的知识,希望对你有一定的参考价值。

PS:其实不用理解透增广路,交替路,网上有对代码的形象解释,看懂也能做题,下面我尽量把原理说清楚

  1. 基本概念部分来源部分来源

    二分图: 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B

    ),则称图G为一个二分图。技术分享图片

    匹配: 一个匹配即一个包含若干条边的集合,且其中任意两条边没有公共端点。下图标红的边即为匹配

技术分享图片

? 最大匹配: 匹配数最大,如下图有4条匹配边技术分享图片

? 完全匹配: 即所有的顶点都是匹配点,上图(Fig.4)即为完全匹配,不是所有的图都存在完全匹配

? 交替路: 从未匹配点出发,依次经过未匹配的边和已匹配的边,即为交替路,如Fig.3:3 -> 5 -> 1 -> 7 -> 4 -> 8

? 增广路(也称增广轨或交错轨): 如果交替路经过除出发点外的另一个未匹配点,则这条交替路称为增广路,如交替路概念的例子,其途径点8,即为增广路

  1. 匈牙利算法

    由增广路的定义推出下面三个结论(设P为一条增广路):

    1). P的路径长度一定为奇数,第一条边和最后一条边都是未匹配的边(根据要途经已匹配的边和要经过另一个未匹配点,这个结论可以理解成第一个点和最后一个点都是未匹配点,可以在Fig.3上的增广路观察到)

    2). P经过取反操作可以得到一个更大的匹配图(取反操作即,未匹配的边变成匹配的边,匹配的边变成未匹配的边,这个结论根据结论1).和交替路概念可得该结论)

    3). 当且仅当不存在关于图M的增广路径,则图M为最大匹配

    算法思路:不停找增广路,并增加匹配的个数

    代码过程模拟(图片来源

    技术分享图片

    技术分享图片

    技术分享图片

    技术分享图片

这里就解释第三幅图到第四幅图的过程,注意这里的过程就是找增广路。图三开始,男3可以匹配女1,(男3 -> 女1),发现女1已经跟男1匹配(男3 -> 女1 -> 男1),看到男1可以跟女2匹配(注意从开始到现在的是未匹配边,匹配边,未匹配边)这样走下去,路径是男3 -> 女1 -> 男1 -> 女2 -> 男2 -> 女3,此时是以未匹配边结束且找不到匹配边了。

求最大匹配的模板

#include <iostream>
#include <algorithm>

using namespace std;

const int MAXN = 105;

int x, y, mp[MAXN][MAXN];
int mx[MAXN], my[MAXN];//x 跟哪个 y 匹配s
int vis[MAXN];//一条增广路中哪些 y 点被访问过

int dfs(int a)
{
    for(int i = 1;i <= n;++i)
    {
        //先找出与 a 点有联系且没被访问过的 y 点
        if(!vis[i] && mp[a][i])
        {
            vis[i] = 1;
            //然后从就是 x -> i -> my[i] 构造这条增广路,接着就是 dfs(my[i]),重复以上操作
            //根据增广路的结论,如果存在增广路,那么最后 my[i] == 0,即为未匹配点
            //然后将其赋新值,根据 DFS 递归各层,该赋值即为增广路中进行取反操作,未匹配边变为匹配边
            //至于匹配边变为未匹配边,my[i] 被赋新值
            //若不存在增广路,那么就没进行赋值,即不取反
            if(!my[i] || dfs(my[i]))
            {
                my[i] = a;
                return 1;
            }
        }
    }
    return 0;
}

int solve()
{
    int ret = 0;
    fill(mx, mx + MAXN, 0);
    fill(my, my + MAXN, 0);
    //刚开始的 x 都是未匹配点
    //vis 每次清零,vis 就是增广路中的 y 点
    for(int i = 1;i <= x;++i)
    {
        fill(vis, vis + MAXN, 0);
        ret += dfs(i);
    }
    return ret;
}

求最大匹配的模板题

#include <iostream>
#include <cstring>

using namespace std;

const int MAXN = 505;//这题数组要开大

int x, y, mp[MAXN][MAXN];
int mx[MAXN], my[MAXN];//x 跟哪个 y 匹配s
int vis[MAXN];//一条增广路中哪些 y 点被访问过

int dfs(int a)
{
    for(int i = 1;i <= y;++i)
    {
        if(!vis[i] && mp[a][i])
        {
            vis[i] = 1;
            if(!my[i] || dfs(my[i]))
            {
                my[i] = a;
                return 1;
            }
        }
    }
    return 0;
}

int solve()
{
    int ret = 0;
    memset(mx, 0, sizeof(mx));
    memset(my, 0, sizeof(my));
    for(int i = 1;i <= x;++i)
    {
        fill(vis, vis + MAXN, 0);
        ret += dfs(i);
    }
    return ret;
}

int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        cin >> x >> y;
        memset(mp, 0, sizeof(mp));
        int num, t;
        for(int i = 1;i <= x;++i)
        {
            cin >> num;
            while(num--)
            {
                cin >> t;
                mp[i][t] = 1;
            }
        }
        if(solve() == x)
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
    }
    return 0;
}

时间复杂度:邻接矩阵最坏O(n ^ 3), 邻接表O(nm)

空间复杂度:邻接矩阵O(n ^ 2), 邻接表O(n + m)

扩展:匈牙利算法也可以看成DFS过程,参考

  1. KM算法

    该算法是求带权二分图的最大权匹配

    此篇易懂,新概念少,没代码,概念看不懂的,建议先看左边易懂那篇再来,这个是代码篇

    顶标:设顶点 Xi 的顶标为 A[i],顶点 Yi 的顶标为 B[j],顶点 Xi 与 Yj 之间的边权为 w[i][j],初始化时,A[i] 的值为与该点关联的最大边权值,B[j] 的值为0

    相等子图:选择 A[i] + B[j] = w[i][j] 的边 <i, j> 构成的子图,就是相等子图

    算法执行过程中,对任一条边<i, j>,A[i] + B[j] >= w[i][j] 恒成立,这个下面图示解释清楚

    这里的算法介绍是用slack[j]数组优化过,slack数组存的数是Y部的点相等子图时,最小要增加的值

    没有优化过的容易理解,代码量也短,去原博客先看完没有优化的会更容易理解优化过的代码

    算法图示:

    给个二分图,只取边权跟顶标相等的边,边权值跟顶标理解为工作效率

    技术分享图片

    看到员工B,B与c以外的其他工作尝试匹配后,顶标和,即上面说的A[i] + B[j] >= w[i][j],并且最小的差值为1,该差值存在slack数组里,当B与c尝试匹配,因为A -> c,就能找到一条增广路,B -> c -> A -> a,此时A只有找到a匹配,但是A -> a 的顶标和 = 4 也是大于边权值 3,与slack数组比较,存储最小的差值1。

    因为能够进行匹配的是相等子图,即不存在此相等子图的减少量,所以遍历未匹配的Y部的slack数组,找出最小的减少量minz。我们要使总的工作效率尽可能地高,即减少地少。

    若X部的点还未匹配,就将增光路中X部减去minz,即下图中A、B都减去1,Y部加上minz,再进行尝试匹配,注意,匹配成功的话,A[i] + B[j] = w[i][j]

    技术分享图片

    其实下面这图不符合匈牙利算法的具体过程,由于这里只要理解结果,就拿下面这图了,这里X部+Y部的顶标值 = 对应点的边权值。正确的连接应该是(看上图),B -> c, A -> a,因为根据匈牙利算法所形成的增广路应该是 B -> c -> A -> a, 在这条增广路中只有 A -> c是匹配的,然后取反,就B -> c, A -> a

    技术分享图片

    由于图到此就错了,所以就不继续贴流程图,直接上结果图,具体过程如上重复技术分享图片

    下面是以上推荐博客的代码,跑了一遍测试,我就把maxn改了

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<vector>
    #include<map>
    using namespace std;
    typedef long long ll;
    const int maxn = 300 + 10;
    const int INF = 0x3f3f3f3f;
    
    int wx[maxn], wy[maxn];//每个点的顶标值(需要根据二分图处理出来)
    int cx[maxn], cy[maxn];//每个点所匹配的点
    int visx[maxn], visy[maxn];//每个点是否加入增广路
    int cntx, cnty;//分别是X和Y的点数
    int Map[maxn][maxn];//二分图边的权值
    int slack[maxn];//边权和顶标最小的差值
    
    bool dfs(int u)//进入DFS的都是X部的点
    {
        visx[u] = 1;//标记进入增广路
        for(int v = 1; v <= cnty; v++)
        {
            if(!visy[v] && Map[u][v] != INF)//如果Y部的点还没进入增广路,并且存在路径
            {
                int t = wx[u] + wy[v] - Map[u][v];
                if(t == 0)//t为0说明是相等子图
                {
                    visy[v] = 1;//加入增广路
    
                    //如果Y部的点还未进行匹配
                    //或者已经进行了匹配,可以从原来的匹配反向找到增广路
                    //那就可以进行匹配
                    if(cy[v] == -1 || dfs(cy[v]))
                    {
                        cx[u] = v;
                        cy[v] = u;//进行匹配
                        return 1;
                    }
                }
                else if(t > 0)//此处t一定是大于0,因为顶标之和一定>=边权
                {
                    slack[v] = min(slack[v], t);
                    //slack[v]存的是Y部的点需要变成相等子图顶标值最小增加多少
                }
            }
        }
        return false;
    }
    
    int KM()
    {
        memset(cx, -1, sizeof(cx));
        memset(cy, -1, sizeof(cy));
        memset(wx, 0, sizeof(wx));//wx的顶标为该点连接的边的最大权值
        memset(wy, 0, sizeof(wy));//wy的顶标为0
        for(int i = 1; i <= cntx; i++)//预处理出顶标值
        {
            for(int j = 1; j <= cnty; j++)
            {
                if(Map[i][j] == INF)continue;
                wx[i] = max(wx[i], Map[i][j]);
            }
        }
        for(int i = 1; i <= cntx; i++)//枚举X部的点
        {
            memset(slack, INF, sizeof(slack));
            while(1)
            {
    
                memset(visx, 0, sizeof(visx));
                memset(visy, 0, sizeof(visy));
                if(dfs(i))break;//已经匹配正确
    
    
                int minz = INF;
                for(int j = 1; j <= cnty; j++)
                    if(!visy[j] && minz > slack[j])
                        //找出还没经过的点中,需要变成相等子图的最小额外增加的顶标值
                        minz = slack[j];
                //和全局变量不同的是,全局变量在每次while循环中都需要赋值成INF,每次求出的是所有点的最小值
                //而slack数组在每个while外面就初始化好,每次while循环slack数组的每个值都在用到
                //在一次增广路中求出的slack值会更准确,循环次数比全局变量更少
    
    
                //还未匹配,将X部的顶标减去minz,Y部的顶标加上minz
                for(int j = 1; j <= cntx; j++)
                    if(visx[j])wx[j] -= minz;
                for(int j = 1; j <= cnty; j++)
                    //修改顶标后,要把所有不在交错树中的Y顶点的slack值都减去minz
                    if(visy[j])wy[j] += minz;
                    else slack[j] -= minz;
            }
        }
    
        int ans = 0;//二分图最优匹配权值
        for(int i = 1; i <= cntx; i++)
            if(cx[i] != -1)ans += Map[i][cx[i]];
        return ans;
    }
    int n, k;
    int main()
    {
        while(scanf("%d", &n) != EOF)
        {
            for(int i = 1; i <= n; i++)
            {
                for(int j = 1; j <= n; j++)
                    scanf("%d", &Map[i][j]);
            }
            cntx = cnty = n;
            printf("%d
    ", KM());
        }
        return 0;
    }
    

以上是关于匈牙利算法KM算法的主要内容,如果未能解决你的问题,请参考以下文章

算法?日更?第五十期二分图(km算法)

KM算法详解[转]

匈牙利与KM算法

匈牙利与KM算法

KM算法原理+证明

二分图最大权完美匹配KM算法模板