[学习笔记] KM算法
前言
这个东西学了我挺久了,我先奉劝各位一定要先搞清楚匈牙利算法。感谢 \(\tt jzm\) 巨佬对我耐心的讲解,因为我太弱了所以卡了很久都不懂。如果你有任何问题请在本篇博客下面留言,我会尽力解答的。
\(\tt KM\) 算法主要用来解决最大权完美匹配,因其稳定的 \(O(n^3)\) 可以吊打玄学费用流,所以出匹配的题时无脑写费用流可能被卡。但是它也只能解决匹配问题,费用流的应用却极其广泛。
我会着重讲解代码,因为我觉得 \(\tt KM\) 的代码才是最难懂的(而且网上的代码很多都有点问题)
思想
神奇的思想,关键是 顶标 这个设计。所谓顶标也就是二分图上的点权,我们把边权转化成点权,设 \(X\) 部的点的顶标为 \(vx[i]\) ,\(Y\) 部的点的顶标为 \(vy[i]\) ,记图上的边为 \(a[i][j]\) ,那么对于任意的 \(i,j\) ,我们要一直保证:
\]
那么对于 \(a[i][j]=vx[i]+vy[j]\) 的边,贪心地选他是最优的。我们成这样的边为 相等边 ,称相等边连接而成的子图为 相等子图 ,那么当相等子图的最大匹配是完美匹配时,说明我们找到了答案,答案是顶标之和。
\(\tt KM\) 算法和匈牙利算法类似,每次都会进行 增广 操作,如果可以直接用相等边增广的话,自然是极好的。如果不能进行增广操作呢?就说明现在的相等子图不行了,我们要 扩大相等子图 。
怎么扩大相等子图?其实就是修改顶标,顶标是可以按照我们的愿望修改的,但是注意要一直满足 \(vx[i]+vy[j]\geq a[i][j]\) ,设我们现在增广到的集合是 \(S\)(既有 \(X\) 部也有 \(Y\) 部),记 \(i\) 是 \(X\) 部的点,\(j\) 是 \(Y\) 部的点,那么对于 \(i\in S\) ,我们使他的顶标减少 \(d\) ,对于 \(j\in S\) ,我们使他的顶标增加 \(d\) ,考虑这样做的影响:
- 对于 \(x\in S,y\in S\) ,\(vx[x]+vy[y]\) 不变,所以还是满足\(vx[x]+vy[x]\geq a[x][y]\)
- 对于 \(x\in S,y\not\in S\) ,这条边 \(vx[x]+vy[y]\) 会减少 \(d\) ,这条边可能就会成为新的相等边。
- 对于 \(x\not\in S,y\in S\) ,这条边 \(vx[x]+vy[y]\) 会增加 \(d\) ,这条边依然不是相等边,但是我们用不到他。
- 对于 \(x\not\in S,y\not\in S\) ,无影响,不用管。
综上所诉,我们可以知道 \(d\) 应该这样取值:
\]
当我们修改顶标之后获得新的相等边之后就可以继续增广了,而且那个限制条件还是一直满足的,挺不错。注意一开始为了满足条件我们应该让 \(vx[i]=\max_j a[i][j]\) ,\(vy[i]=0\)
代码
就算懂了思想之后可能还是难以敲出代码,先给出一些数组的定义:
- \(vx[i],vy[i]\) ,顶标数组,如上所述。
- \(xtoy[i],ytox[i]\) ,表示和 \(X/Y\) 部的点匹配的是哪个点,如果没有匹配点就是 \(0\)
- \(visx[i],visy[i]\) ,表示在当前增广过程中这个点是否被访问到。
- \(pre[i]\) 表示 \(Y\) 部的点想匹配其的 \(X\) 部的点 ,理解这个数组很重要,就是匈牙利算法的思想,\(X\) 部的点 \(a\) 虽然想匹配他,但是碍于这个点已经有了一个匹配的 \(X\) 部的点 \(b\) ,所以我们先记录下这个数组,再去增广 \(b\) ,因为写法是 \(\tt bfs\) 的,所以用这个数组来达到 \(\tt dfs\) 的功能。
- \(sy[i]\) 表示还没有访问的 \(Y\) 部的点的 \(\min_{x_\in S}vx[x]+vy[y]-a[x][y]\) ,这个数组方便算 \(d\) ,而且维护起来也不是很麻烦,在每次 \(X\) 部的点加入 \(S\) 集合时修改一下它就行了。
请先完全理解上面的定义之后再读我带注释的代码,就是这道题【模板】二分图最大权完美匹配
如果你读懂了代码,那么不难知道复杂度是 \(O(n^3+nm)\) ,因为均摊下来花费 \(O(n)\) 的时间可以使一条边变成相等边,最差情况当所有边变成相等边时一定可以得到完美匹配,\(O(n^3)\) 就是每次枚举增广哪个点,二分图的每个点又要扩展一次,扩展的消耗时 \(O(n)\) ,所以总共时 \(O(n^3)\) 的。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
#define int long long
const int inf = 1e18;
const int M = 505;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,vx[M],vy[M],xtoy[M],ytox[M];
int visx[M],visy[M],pre[M],sy[M],a[M][M];
void aug(int y)
//也就是y这个点是空闲的,可以把以前记录的匹配修改了,可以增加一个匹配
{
for(int nxt,x;y;y=nxt)//我觉得这个函数很像"链式反应"
{
nxt=xtoy[x=pre[y]];
//nxt表示x的原配y,由于x已经找到了新的y所以原配就被解放出来了
ytox[y]=x;xtoy[x]=y;//修改匹配关系
}
}
void jzm(int x)//每次都只解决x的问题,就和匈牙利很类似啊
{
queue<int> q;
memset(visx,0,sizeof visx);//一定要清空
memset(visy,0,sizeof visy);//一定要情况
for(int i=1;i<=n;i++) sy[i]=inf;//一开始都是最大值,注意一下定义哦
q.push(x);
while(1)//直到找到了x的匹配再离开
{
while(!q.empty())
{
int u=q.front();q.pop();
visx[u]=1;
for(int i=1;i<=n;i++)
{
if(visy[i] || a[u][i]==-inf) continue;
//如果已经在S中或者边没有,不能访问这个y
int d=vx[u]+vy[i]-a[u][i];//算d
if(d==0)//直接就是相等边了
{
visy[i]=1;pre[i]=u;//这个点就被增广过了,点u是想匹配i的,所以修改pre
if(!ytox[i]) {aug(i);return ;}//这个点空闲,可以直接达到目的,做完之后就走了
else q.push(ytox[i]);//否则继续从它的原配开始找
}
else if(sy[i]>d)//不是相等边,所以可以修改sy
sy[i]=d,pre[i]=u;//如果这个相等边被激活了,那么u就想匹配i,多打了标记也没关系
}
}
int del=inf;//求那个min(....),就是修改量
for(int i=1;i<=n;i++)
if(!visy[i]) del=min(del,sy[i]);//只能在没有访问的y里面找
for(int i=1;i<=n;i++)//这里就是按照定义修改顶标
{
if(visx[i]) vx[i]-=del;
if(visy[i]) vy[i]+=del;
else if(sy[i]!=inf) sy[i]-=del;
//如果sy有意义,那么他一定被哪个x访问过,那么由于vx的减少,他就一定会减小
}
for(int i=1;i<=n;i++)
if(!visy[i] && !sy[i])//再修改顶标之后成为了相等边
{
visy[i]=1;//和上面类似的操作,扩展
if(!ytox[i]) {aug(i);return ;}
else q.push(ytox[i]);
}
}
}
int KM()
{
int ans=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
vx[i]=max(vx[i],a[i][j]);//初始的vx
for(int i=1;i<=n;i++)//每次就增广一个点
jzm(i);
for(int i=1;i<=n;i++)
ans+=vx[i]+vy[i];//最后的答案一定是这个
return ans;
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]=-inf;//注意要初始化
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),c=read();
a[u][v]=max(a[u][v],c);//选最大的边
}
printf("%lld\n",KM());
for(int i=1;i<=n;i++)
printf("%lld ",ytox[i]);
}
应用
注意我再 前言
中说它可以解决匹配问题,所以不只能解决最大权完美匹配问题哦,可以看看 oneindark 巨佬博客的最后一部分,然后如果我做到了更多关于 \(\tt KM\) 的题是会补充到这里的:
[学习笔记] KM算法的更多相关文章
- [ML学习笔记] XGBoost算法
[ML学习笔记] XGBoost算法 回归树 决策树可用于分类和回归,分类的结果是离散值(类别),回归的结果是连续值(数值),但本质都是特征(feature)到结果/标签(label)之间的映射. 这 ...
- 学习笔记 - Manacher算法
Manacher算法 - 学习笔记 是从最近Codeforces的一场比赛了解到这个算法的~ 非常新奇,毕竟是第一次听说 \(O(n)\) 的回文串算法 我在 vjudge 上开了一个[练习],有兴趣 ...
- 学习笔记——EM算法
EM算法是一种迭代算法,用于含有隐变量(hidden variable)的概率模型参数的极大似然估计,或极大后验概率估计.EM算法的每次迭代由两步组成:E步,求期望(expectation):M步,求 ...
- 数据挖掘学习笔记--AdaBoost算法(一)
声明: 这篇笔记是自己对AdaBoost原理的一些理解,如果有错,还望指正,俯谢- 背景: AdaBoost算法,这个算法思路简单,但是论文真是各种晦涩啊-,以下是自己看了A Short Introd ...
- 学习笔记-KMP算法
按照学习计划和TimeMachine学长的推荐,学习了一下KMP算法. 昨晚晚自习下课前粗略的看了看,发现根本理解不了高端的next数组啊有木有,不过好在在今天系统的学习了之后感觉是有很大提升的了,起 ...
- Java学习笔记——排序算法之快速排序
会当凌绝顶,一览众山小. --望岳 如果说有哪个排序算法不能不会,那就是快速排序(Quick Sort)了 快速排序简单而高效,是最适合学习的进阶排序算法. 直接上代码: public class Q ...
- Java学习笔记——排序算法之进阶排序(堆排序与分治并归排序)
春蚕到死丝方尽,蜡炬成灰泪始干 --无题 这里介绍两个比较难的算法: 1.堆排序 2.分治并归排序 先说堆. 这里请大家先自行了解完全二叉树的数据结构. 堆是完全二叉树.大顶堆是在堆中,任意双亲值都大 ...
- Java学习笔记——排序算法之希尔排序(Shell Sort)
落日楼头,断鸿声里,江南游子.把吴钩看了,栏杆拍遍,无人会,登临意. --水龙吟·登建康赏心亭 希尔算法是希尔(D.L.Shell)于1959年提出的一种排序算法.是第一个时间复杂度突破O(n²)的算 ...
- 学习笔记——SM2算法原理及实现
RSA算法的危机在于其存在亚指数算法,对ECC算法而言一般没有亚指数攻击算法 SM2椭圆曲线公钥密码算法:我国自主知识产权的商用密码算法,是ECC(Elliptic Curve Cryptosyste ...
随机推荐
- Zabbix 触发器配置多监控项阈值
配置内存自定义监控项 # 监控内存命令 [root@web01 ~]# free -m|awk '/^Mem/{print $NF/$2}' 0.664609 [root@web01 ~]# free ...
- 二进制方式安装docker(非root用户启动docker)
二进制方式安装docker(非root用户启动docker) 一.下载安装包: 地址:https://download.docker.com/linux/static/stable/x86_64/ 这 ...
- 007.NET5 Log4Net组件使用
NET 5 Log4Net组件使用 1. Nuget引入程序集:log4net + Microsfot.Extensions.Logging.Log4Net.AspNetCore 2. 准备配置文件 ...
- IDEA插件:快速删除Java代码中的注释
背景 有时,我们需要删除Java源代码中的注释.目前有不少方法,比如: 实现状态机.该方式较为通用,适用于多种语言(取决于状态机支持的注释符号). 正则匹配.该方式容易误判,尤其是容易误删字符串. ...
- 使用 js 实现十大排序算法: 桶排序
使用 js 实现十大排序算法: 桶排序 桶排序 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!
- how to config custom process.env in node.js
how to config custom process.env in node.js process.env APP_ENV NODE_ENV https://nodejs.org/api/proc ...
- 微信公众号 & 付费阅读
微信公众号 & 付费阅读 付费功能 付费阅读 付费功能使用说明 1.付费功能介绍 开通了付费功能的公众号,运营者可以在编辑时对原创文章的部分或全部内容设置收费.对于付费图文,用户未付费前可免费 ...
- Clean Code of JavaScript
Clean Code of JavaScript 代码简洁之道 JavaScript 版 https://github.com/ryanmcdermott/clean-code-javascript ...
- Dart: 请求graphql数据
import 'package:http/http.dart' as http; const url = "http://127.0.0.1:4000/graphql"; main ...
- NGK:价值对标比特币,上线暴涨4558%,下一个财富暴增风口
近期,美股行情多变,一直饱受争议的比特币也成了其中的"弄潮儿".看多者认为,机构的兴趣有助于支撑比特币作为对冲美元疲软和通胀的工具. 特别是今年1月底的时候,马斯克将推特简介更改为 ...