传送门: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数组

 #include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<map>
using namespace std;
typedef long long ll;
const int maxn = + ;
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 minz;//边权和顶标最小的差值 bool dfs(int u)//进入DFS的都是X部的点
{
visx[u] = ;//标记进入增广路
for(int v = ; v <= cnty; v++)
{
if(!visy[v] && Map[u][v] != INF)//如果Y部的点还没进入增广路,并且存在路径
{
int t = wx[u] + wy[v] - Map[u][v];
if(t == )//t为0说明是相等子图
{
visy[v] = ;//加入增广路 //如果Y部的点还未进行匹配
//或者已经进行了匹配,可以从原来的匹配反向找到增广路
//那就可以进行匹配
if(cy[v] == - || dfs(cy[v]))
{
cx[u] = v;
cy[v] = u;//进行匹配
return ;
}
}
else if(t > )//此处t一定是大于0,因为顶标之和一定>=边权
{
minz = min(minz, t);//边权和顶标最小的差值
}
}
}
return false;
} int KM()
{
memset(cx, -, sizeof(cx));
memset(cy, -, sizeof(cy));
memset(wx, , sizeof(wx));//wx的顶标为该点连接的边的最大权值
memset(wy, , sizeof(wy));//wy的顶标为0
for(int i = ; i <= cntx; i++)//预处理出顶标值
{
for(int j = ; j <= cnty; j++)
{
if(Map[i][j] == INF)continue;
wx[i] = max(wx[i], Map[i][j]);
}
}
for(int i = ; i <= cntx; i++)//枚举X部的点
{
while()
{
minz = INF;
memset(visx, , sizeof(visx));
memset(visy, , sizeof(visy));
if(dfs(i))break;//已经匹配正确 //还未匹配,将X部的顶标减去minz,Y部的顶标加上minz
for(int j = ; j <= cntx; j++)
if(visx[j])wx[j] -= minz;
for(int j = ; j <= cnty; j++)
if(visy[j])wy[j] += minz;
}
} int ans = ;//二分图最优匹配权值
for(int i = ; i <= cntx; i++)
if(cx[i] != -)ans += Map[i][cx[i]];
return ans;
}
int n, k;
int main()
{
while(scanf("%d", &n) != EOF)
{
for(int i = ; i <= n; i++)
{
for(int j = ; j <= n; j++)
scanf("%d", &Map[i][j]);
}
cntx = cnty = n;
printf("%d\n", KM());
}
return ;
}

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

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

 #include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<map>
using namespace std;
typedef long long ll;
const int maxn = + ;
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] = ;//标记进入增广路
for(int v = ; v <= cnty; v++)
{
if(!visy[v] && Map[u][v] != INF)//如果Y部的点还没进入增广路,并且存在路径
{
int t = wx[u] + wy[v] - Map[u][v];
if(t == )//t为0说明是相等子图
{
visy[v] = ;//加入增广路 //如果Y部的点还未进行匹配
//或者已经进行了匹配,可以从原来的匹配反向找到增广路
//那就可以进行匹配
if(cy[v] == - || dfs(cy[v]))
{
cx[u] = v;
cy[v] = u;//进行匹配
return ;
}
}
else if(t > )//此处t一定是大于0,因为顶标之和一定>=边权
{
slack[v] = min(slack[v], t);
//slack[v]存的是Y部的点需要变成相等子图顶标值最小增加多少
}
}
}
return false;
} int KM()
{
memset(cx, -, sizeof(cx));
memset(cy, -, sizeof(cy));
memset(wx, , sizeof(wx));//wx的顶标为该点连接的边的最大权值
memset(wy, , sizeof(wy));//wy的顶标为0
for(int i = ; i <= cntx; i++)//预处理出顶标值
{
for(int j = ; j <= cnty; j++)
{
if(Map[i][j] == INF)continue;
wx[i] = max(wx[i], Map[i][j]);
}
}
for(int i = ; i <= cntx; i++)//枚举X部的点
{
memset(slack, INF, sizeof(slack));
while()
{ memset(visx, , sizeof(visx));
memset(visy, , sizeof(visy));
if(dfs(i))break;//已经匹配正确 int minz = INF;
for(int j = ; 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 = ; j <= cntx; j++)
if(visx[j])wx[j] -= minz;
for(int j = ; j <= cnty; j++)
//修改顶标后,要把所有不在交错树中的Y顶点的slack值都减去minz
if(visy[j])wy[j] += minz;
else slack[j] -= minz;
}
} int ans = ;//二分图最优匹配权值
for(int i = ; i <= cntx; i++)
if(cx[i] != -)ans += Map[i][cx[i]];
return ans;
}
int n, k;
int main()
{
while(scanf("%d", &n) != EOF)
{
for(int i = ; i <= n; i++)
{
for(int j = ; j <= n; j++)
scanf("%d", &Map[i][j]);
}
cntx = cnty = n;
printf("%d\n", KM());
}
return ;
}

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. 运行monitor提示需要安装旧JAVA SE 6运行环境

    MAC系统下运行monitor命令 ➜ tools git:(master) ✗ monitor 提示如下: 若要打开Eclipse.app,您需要Java SE 6 runtime,您想现在安装一个 ...

  2. DataAnnotationsModelValidator-基于数据注解方式的model验证器

    http://www.cnblogs.com/artech/archive/2012/04/10/how-mvc-works.html http://www.cnblogs.com/artech/ar ...

  3. 当Linux用尽内存

    Mulyadi Santosa 也许你很少面临这一情况,但是一旦如此,你一定知道出什么错了:可用内存不足或者说内存用尽(OOM).结果非常典型:你不能再分配内存,内核会杀掉一个任务(一般是正在运行那个 ...

  4. Struts2 校验数据问题

    我们会经常遇到一下问题,例如我在前端输入数据,把数据发送到和后台,我首先要校验这个数据, 比如说:前端必须输入一个日期类型的数据,后端才能正确接收,要是输入一个不是日期型的数据, 那么后端就要把数据打 ...

  5. 编写高质量代码改善C#程序的157个建议——建议6: 区别readonly和const的使用方法

    建议6: 区别readonly和const的使用方法 很多初学者分不清readonly和const的使用场合.在我看来,要使用const的理由只有一个,那就是效率.但是,在大部分应用情况下, “效率” ...

  6. html manifest 离线配置

    HTML5 引入了应用程序缓存,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问. 应用程序缓存为应用带来三个优势: 离线浏览 - 用户可在应用离线时使用它们 速度 - 已缓存资源加载 ...

  7. 接上一篇,Springcloud使用feignclient远程调用服务404 ,为什么去掉context-path后,就能够调通

    一.问题回顾 如果application.properties文件中配置了 #项目路径 server.servlet.context-path=/pear-cache-service 则feigncl ...

  8. 和Webbrowser进行简单交互

    作为第一篇,简单的控件使用就不说了. 直接从简单的交互开始吧! C#使用网页中已有的js函数 webBrowser.Document.InvokeScript("Stop");// ...

  9. 《Servlet和jsp学习指南》 笔记2

    chapter 13 请求和响应的装饰 初步了解Decorator模式: 在不修改一个对象的类的源码的情况下,装饰这个对象的行为. chapter 14 异步处理 异步Servlet和Filter,只 ...

  10. 51 nod 1350 斐波那契表示

    每一个正整数都可以表示为若干个斐波那契数的和,一个整数可能存在多种不同的表示方法,例如:14 = 13 + 1 = 8 + 5 + 1,其中13 + 1是最短的表示(只用了2个斐波那契数).定义F(n ...