传送门:KM算法---理解篇

最佳匹配

什么是完美匹配

如果一个二分图,X部和Y部的顶点数相等,若存在一个匹配包含X部与Y部的所有顶点,则称为完美匹配。
换句话说:若二分图X部的每一个顶点都与Y中的一个顶点匹配,**并且**Y部中的每一个顶点也与X部中的一个顶点匹配,则该匹配为完美匹配。

什么是完备匹配

如果一个二分图,X部中的每一个顶点都与Y部中的一个顶点匹配,**或者**Y部中的每一个顶点也与X部中的一个顶点匹配,则该匹配为完备匹配。

什么是最佳匹配

带权二分图的权值最大完备匹配称为最佳匹配。

二分图的最佳匹配不一定是二分图的最大权匹配。

转化

可以添加一些权值为0的边,使得最佳匹配和最大权匹配统一起来。

KM算法

求二分图的最佳匹配有一个非常优秀的算法,可以做到O(N^3),这就是KM算法。该算法描述如下:

1.首先选择顶点数较少的为X部,初始时对X部的每一个顶点设置顶标,顶标的值为该点关联的最大边的权值,Y部的顶点顶标为0。

2.对于X部中的每个顶点,在相等子图中利用匈牙利算法找一条增广路径,如果没有找到,则修改顶标,扩大相等子图,继续找增广路径。当每个点都找到增广路径时,此时意味着每个点都在匹配中,即找到了二分图的完备匹配。该完备匹配即为二分图的最佳匹配。

什么是相等子图呢?因为每个顶点有一个顶标,如果我们选择边权等于两端点的顶标之和的边,它们组成的图称为相等子图。

如果从X部中的某个点Xi出发在相等子图中没有找到增广路径,我们是如何修改顶标的呢?如果我们没有找到增广路径,则我们一定找到了许多条从Xi出发并结束于X部的匹配边与未匹配边交替出现的路径,姑且称之为交错树。我们将交错树中X部的顶点顶标减去一个值d,交错树中属于Y部的顶点顶标加上一个值d。这个值后面要讲它如何计算。那么我们会发现:

  • 两端都在交错树中的边(i,j),其顶标和没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。

  • 两端都不在交错树中的边(i,j),其顶标也没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。

  • X端不在交错树中,Y端在交错树中的边(i,j),它的顶标和会增大。它原来不属于相等子图,现在仍不属于相等子图。

  • X端在交错树中,Y端不在交错树中的边(i,j),它的顶标和会减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。

  • 我们修改顶标的目的就是要扩大相等子图。为了保证至少有一条边进入相等子图,我们可以在交错树的边中寻找顶标和与边权之差最小的边,这就是前面说的d值。将交错树中属于X部的顶点减去d,交错树中属于Y部的顶点加上d。则可以保证至少有一条边扩充进入相等子图。

3.当X部的所有顶点都找到了增广路径后,则找到了完备匹配,此完备匹配即为最佳匹配。

相等子图的若干性质

    1. 在任意时刻,相等子图上的最大权匹配一定小于等于相等子图的顶标和。
    2. 在任意时刻,相等子图的顶标和即为所有顶点的顶标和。
    3. 扩充相等子图后,相等子图的顶标和将会减小。
    4. 当相等子图的最大匹配为原图的完备匹配时,匹配边的权值和等于所有顶点的顶标和,此匹配即为最佳匹配

以上就是KM算法的基本思路。但是朴素的实现方法,时间复杂度为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值即可。但还要注意一点:修 改顶标后,要把所有不在交错树中的Y顶点的slack值都减去d

模板一,用全局变量minz表示边权和顶标最小的差值,省去slack数组

  1. #include<iostream>
  2. #include<cstring>
  3. #include<cstdio>
  4. #include<vector>
  5. #include<map>
  6. using namespace std;
  7. typedef long long ll;
  8. const int maxn = + ;
  9. const int INF = 0x3f3f3f3f;
  10.  
  11. int wx[maxn], wy[maxn];//每个点的顶标值(需要根据二分图处理出来)
  12. int cx[maxn], cy[maxn];//每个点所匹配的点
  13. int visx[maxn], visy[maxn];//每个点是否加入增广路
  14. int cntx, cnty;//分别是X和Y的点数
  15. int Map[maxn][maxn];//二分图边的权值
  16. int minz;//边权和顶标最小的差值
  17.  
  18. bool dfs(int u)//进入DFS的都是X部的点
  19. {
  20. visx[u] = ;//标记进入增广路
  21. for(int v = ; v <= cnty; v++)
  22. {
  23. if(!visy[v] && Map[u][v] != INF)//如果Y部的点还没进入增广路,并且存在路径
  24. {
  25. int t = wx[u] + wy[v] - Map[u][v];
  26. if(t == )//t为0说明是相等子图
  27. {
  28. visy[v] = ;//加入增广路
  29.  
  30. //如果Y部的点还未进行匹配
  31. //或者已经进行了匹配,可以从原来的匹配反向找到增广路
  32. //那就可以进行匹配
  33. if(cy[v] == - || dfs(cy[v]))
  34. {
  35. cx[u] = v;
  36. cy[v] = u;//进行匹配
  37. return ;
  38. }
  39. }
  40. else if(t > )//此处t一定是大于0,因为顶标之和一定>=边权
  41. {
  42. minz = min(minz, t);//边权和顶标最小的差值
  43. }
  44. }
  45. }
  46. return false;
  47. }
  48.  
  49. int KM()
  50. {
  51. memset(cx, -, sizeof(cx));
  52. memset(cy, -, sizeof(cy));
  53. memset(wx, , sizeof(wx));//wx的顶标为该点连接的边的最大权值
  54. memset(wy, , sizeof(wy));//wy的顶标为0
  55. for(int i = ; i <= cntx; i++)//预处理出顶标值
  56. {
  57. for(int j = ; j <= cnty; j++)
  58. {
  59. if(Map[i][j] == INF)continue;
  60. wx[i] = max(wx[i], Map[i][j]);
  61. }
  62. }
  63. for(int i = ; i <= cntx; i++)//枚举X部的点
  64. {
  65. while()
  66. {
  67. minz = INF;
  68. memset(visx, , sizeof(visx));
  69. memset(visy, , sizeof(visy));
  70. if(dfs(i))break;//已经匹配正确
  71.  
  72. //还未匹配,将X部的顶标减去minz,Y部的顶标加上minz
  73. for(int j = ; j <= cntx; j++)
  74. if(visx[j])wx[j] -= minz;
  75. for(int j = ; j <= cnty; j++)
  76. if(visy[j])wy[j] += minz;
  77. }
  78. }
  79.  
  80. int ans = ;//二分图最优匹配权值
  81. for(int i = ; i <= cntx; i++)
  82. if(cx[i] != -)ans += Map[i][cx[i]];
  83. return ans;
  84. }
  85. int n, k;
  86. int main()
  87. {
  88. while(scanf("%d", &n) != EOF)
  89. {
  90. for(int i = ; i <= n; i++)
  91. {
  92. for(int j = ; j <= n; j++)
  93. scanf("%d", &Map[i][j]);
  94. }
  95. cntx = cnty = n;
  96. printf("%d\n", KM());
  97. }
  98. return ;
  99. }

模板二,用slack数组(对于完全图的优化很快)

和全局变量不同的是,全局变量在每次while循环中都需要赋值成INF,每次求出的是所有点的最小值,而slack数组在每个while外面就初始化好,每次while循环slack数组的每个值都在用到,一次增广路中求出的slack值会更准确,循环次数比全局变量更少

  1. #include<iostream>
  2. #include<cstring>
  3. #include<cstdio>
  4. #include<vector>
  5. #include<map>
  6. using namespace std;
  7. typedef long long ll;
  8. const int maxn = + ;
  9. const int INF = 0x3f3f3f3f;
  10.  
  11. int wx[maxn], wy[maxn];//每个点的顶标值(需要根据二分图处理出来)
  12. int cx[maxn], cy[maxn];//每个点所匹配的点
  13. int visx[maxn], visy[maxn];//每个点是否加入增广路
  14. int cntx, cnty;//分别是X和Y的点数
  15. int Map[maxn][maxn];//二分图边的权值
  16. int slack[maxn];//边权和顶标最小的差值
  17.  
  18. bool dfs(int u)//进入DFS的都是X部的点
  19. {
  20. visx[u] = ;//标记进入增广路
  21. for(int v = ; v <= cnty; v++)
  22. {
  23. if(!visy[v] && Map[u][v] != INF)//如果Y部的点还没进入增广路,并且存在路径
  24. {
  25. int t = wx[u] + wy[v] - Map[u][v];
  26. if(t == )//t为0说明是相等子图
  27. {
  28. visy[v] = ;//加入增广路
  29.  
  30. //如果Y部的点还未进行匹配
  31. //或者已经进行了匹配,可以从原来的匹配反向找到增广路
  32. //那就可以进行匹配
  33. if(cy[v] == - || dfs(cy[v]))
  34. {
  35. cx[u] = v;
  36. cy[v] = u;//进行匹配
  37. return ;
  38. }
  39. }
  40. else if(t > )//此处t一定是大于0,因为顶标之和一定>=边权
  41. {
  42. slack[v] = min(slack[v], t);
  43. //slack[v]存的是Y部的点需要变成相等子图顶标值最小增加多少
  44. }
  45. }
  46. }
  47. return false;
  48. }
  49.  
  50. int KM()
  51. {
  52. memset(cx, -, sizeof(cx));
  53. memset(cy, -, sizeof(cy));
  54. memset(wx, , sizeof(wx));//wx的顶标为该点连接的边的最大权值
  55. memset(wy, , sizeof(wy));//wy的顶标为0
  56. for(int i = ; i <= cntx; i++)//预处理出顶标值
  57. {
  58. for(int j = ; j <= cnty; j++)
  59. {
  60. if(Map[i][j] == INF)continue;
  61. wx[i] = max(wx[i], Map[i][j]);
  62. }
  63. }
  64. for(int i = ; i <= cntx; i++)//枚举X部的点
  65. {
  66. memset(slack, INF, sizeof(slack));
  67. while()
  68. {
  69.  
  70. memset(visx, , sizeof(visx));
  71. memset(visy, , sizeof(visy));
  72. if(dfs(i))break;//已经匹配正确
  73.  
  74. int minz = INF;
  75. for(int j = ; j <= cnty; j++)
  76. if(!visy[j] && minz > slack[j])
  77. //找出还没经过的点中,需要变成相等子图的最小额外增加的顶标值
  78. minz = slack[j];
  79. //和全局变量不同的是,全局变量在每次while循环中都需要赋值成INF,每次求出的是所有点的最小值
  80. //而slack数组在每个while外面就初始化好,每次while循环slack数组的每个值都在用到
  81. //在一次增广路中求出的slack值会更准确,循环次数比全局变量更少
  82.  
  83. //还未匹配,将X部的顶标减去minz,Y部的顶标加上minz
  84. for(int j = ; j <= cntx; j++)
  85. if(visx[j])wx[j] -= minz;
  86. for(int j = ; j <= cnty; j++)
  87. //修改顶标后,要把所有不在交错树中的Y顶点的slack值都减去minz
  88. if(visy[j])wy[j] += minz;
  89. else slack[j] -= minz;
  90. }
  91. }
  92.  
  93. int ans = ;//二分图最优匹配权值
  94. for(int i = ; i <= cntx; i++)
  95. if(cx[i] != -)ans += Map[i][cx[i]];
  96. return ans;
  97. }
  98. int n, k;
  99. int main()
  100. {
  101. while(scanf("%d", &n) != EOF)
  102. {
  103. for(int i = ; i <= n; i++)
  104. {
  105. for(int j = ; j <= n; j++)
  106. scanf("%d", &Map[i][j]);
  107. }
  108. cntx = cnty = n;
  109. printf("%d\n", KM());
  110. }
  111. return ;
  112. }

KM算法(运用篇)的更多相关文章

  1. KM算法(理解篇)

    转载:https://www.cnblogs.com/logosG/p/logos.html(很好,很容易理解) 一.匈牙利算法 匈牙利算法用于解决什么问题? 匈牙利算法用于解决二分图的最大匹配问题. ...

  2. KM算法萌新讲解篇

    KM算法   首先了解问题:也就是最大权值匹配: 二分图里,边带了权值,求整幅图里匹配最大/最小的权值 因为接触匈牙利算法的时候看的是找对象系列的博文,所以也自己写一发找对象的博文吧: 算法背景: 信 ...

  3. KM算法及其优化的学习笔记&&bzoj2539: [Ctsc2000]丘比特的烦恼

    感谢  http://www.cnblogs.com/vongang/archive/2012/04/28/2475731.html 这篇blog里提供了3个链接……基本上很明白地把KM算法是啥讲清楚 ...

  4. 匈牙利算法、KM算法

    PS:其实不用理解透增广路,交替路,网上有对代码的形象解释,看懂也能做题,下面我尽量把原理说清楚 基本概念 (部分来源.部分来源) 二分图: 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相 ...

  5. HDU-2255(KM算法)

    HDU-2255 题目意思转化之后就是,给你一个二分图(也称 二部图) ,要求选择一些边让左边的点都对应左边的某一个点!该问题也叫做二分图最大匹配.所以可以用KM算法来做这道题.KM前提你要理解匈牙利 ...

  6. 二分图最大权完美匹配KM算法

    KM算法二分图 KM求得二分图与普通二分图的不同之处在于:此二分图的每条边(男生女生)上都附了权值(好感度).然后,求怎样完美匹配使得权值之和最大. 这,不止一般的麻烦啊. 可以通过一个期望值来求. ...

  7. 二分图最大权匹配——KM算法

    前言 这东西虽然我早就学过了,但是最近才发现我以前学的是假的,心中感慨万千(雾),故作此篇. 简介 带权二分图:每条边都有权值的二分图 最大权匹配:使所选边权和最大的匹配 KM算法,全称Kuhn-Mu ...

  8. 【7.18总结】KM算法

    先贴代码,参考博客来源于:https://blog.csdn.net/zyy173533832/article/details/11519291#commentBox 例题:HDU 2255 题意:n ...

  9. KM 算法

    KM 算法 可能需要先去学学匈牙利算法等二分图相关知识. 模板题-洛谷P6577 [模板]二分图最大权完美匹配 给 \(n\) 和 \(m\) 与边 \(u_i,v_i,w_i(1\le i\le m ...

随机推荐

  1. App测试从入门到精通之功能测试

    App的功能测试指的是针对软件需求以及用户要求针对APP功能进行测试.简单点理解就是保证App功能的正确性,不要系统出现Bug.让用户用户的舒服,用的爽!好了,我们看下关于App的功能测试要点有哪些? ...

  2. URAL 1104 Don’t Ask Woman about Her Age(数论)

    题目链接 题意 : 给你一个数,未知进制,然后让你从2到36进制中找出一个最小的进制K,满足给你的这个数作为k进制时能够整除k-1. 思路 : 有一个公式,(a*b^n)mod(b-1)=a: 给定你 ...

  3. Storm的wordCounter计数器详解

    原文:http://www.maoxiangyi.cn/index.php/archives/362 拓扑 点击(此处)折叠或打开 package cn.jd.storm; import backty ...

  4. 【Java学习01】:win7环境安装JDK

    最近开始学习java,从第一步做起吧.win7中安装jdk非常简单,大牛勿喷. 1.下载JDK并执行安装. 访问网站www.java.com,选择win7版本的JDK下载.下载后双击安装(比较慢,耐心 ...

  5. C# enum 枚举 反射

    枚举遍历 public enum EMyType { [System.ComponentModel.Description("A类型")] TypeA = 1, [System.C ...

  6. Android TV 开发 (1)

    本文来自网易云社区 作者:孙有军 前言 这里主要记录几个TV问题的解决方案,如果对这个不感兴趣的其实就不用往下看了. 这几天有一个需求就是要求出一个TV版本的app,之前没有具体的了解Tv版的app有 ...

  7. ST表略解

    题面 给定一个长度为\(N\)的数列,和\(M\)次询问,求出每一次询问的区间内数字的最大值. 对于30%的数据,满足: \(1≤N,M≤10\) 对于70%的数据,满足: \(1≤N,M≤10^5\ ...

  8. 6w6:第六周程序填空题3

    描述 下面的程序输出结果是: A::Fun A::Do A::Fun C::Do 请填空: #include <iostream> using namespace std; class A ...

  9. Django 实现上传图片功能

    很多时候我们要用到图片上传功能,如果图片一直用放在别的网站上,通过加载网址的方式来显示的话其实也挺麻烦的,我们通过使用 django-filer 这个模块实现将图片文件直接放在自己的网站上. 感兴趣的 ...

  10. Web 安全入门-书籍及建议

    https://www.jianshu.com/p/6dcebd54fb24 (本文源于转载或摘抄整理) 2016-06-12 Fooying 优主张 最近比较忙,灵感稍微有点缺乏,本着宁缺毋滥的想法 ...