前言

这个东西学了我挺久了,我先奉劝各位一定要先搞清楚匈牙利算法。感谢 \(\tt jzm\) 巨佬对我耐心的讲解,因为我太弱了所以卡了很久都不懂。如果你有任何问题请在本篇博客下面留言,我会尽力解答的。

\(\tt KM\) 算法主要用来解决最大权完美匹配,因其稳定的 \(O(n^3)\) 可以吊打玄学费用流,所以出匹配的题时无脑写费用流可能被卡。但是它也只能解决匹配问题,费用流的应用却极其广泛。

我会着重讲解代码,因为我觉得 \(\tt KM\) 的代码才是最难懂的(而且网上的代码很多都有点问题)

思想

神奇的思想,关键是 顶标 这个设计。所谓顶标也就是二分图上的点权,我们把边权转化成点权,设 \(X\) 部的点的顶标为 \(vx[i]\) ,\(Y\) 部的点的顶标为 \(vy[i]\) ,记图上的边为 \(a[i][j]\) ,那么对于任意的 \(i,j\) ,我们要一直保证:

\[vx[i]+vy[j]\geq a[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\) 应该这样取值:

\[d=\min_{x\in S,y\not\in S} vx[x]+vy[y]-a[x][y]
\]

当我们修改顶标之后获得新的相等边之后就可以继续增广了,而且那个限制条件还是一直满足的,挺不错。注意一开始为了满足条件我们应该让 \(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算法的更多相关文章

  1. [ML学习笔记] XGBoost算法

    [ML学习笔记] XGBoost算法 回归树 决策树可用于分类和回归,分类的结果是离散值(类别),回归的结果是连续值(数值),但本质都是特征(feature)到结果/标签(label)之间的映射. 这 ...

  2. 学习笔记 - Manacher算法

    Manacher算法 - 学习笔记 是从最近Codeforces的一场比赛了解到这个算法的~ 非常新奇,毕竟是第一次听说 \(O(n)\) 的回文串算法 我在 vjudge 上开了一个[练习],有兴趣 ...

  3. 学习笔记——EM算法

    EM算法是一种迭代算法,用于含有隐变量(hidden variable)的概率模型参数的极大似然估计,或极大后验概率估计.EM算法的每次迭代由两步组成:E步,求期望(expectation):M步,求 ...

  4. 数据挖掘学习笔记--AdaBoost算法(一)

    声明: 这篇笔记是自己对AdaBoost原理的一些理解,如果有错,还望指正,俯谢- 背景: AdaBoost算法,这个算法思路简单,但是论文真是各种晦涩啊-,以下是自己看了A Short Introd ...

  5. 学习笔记-KMP算法

    按照学习计划和TimeMachine学长的推荐,学习了一下KMP算法. 昨晚晚自习下课前粗略的看了看,发现根本理解不了高端的next数组啊有木有,不过好在在今天系统的学习了之后感觉是有很大提升的了,起 ...

  6. Java学习笔记——排序算法之快速排序

    会当凌绝顶,一览众山小. --望岳 如果说有哪个排序算法不能不会,那就是快速排序(Quick Sort)了 快速排序简单而高效,是最适合学习的进阶排序算法. 直接上代码: public class Q ...

  7. Java学习笔记——排序算法之进阶排序(堆排序与分治并归排序)

    春蚕到死丝方尽,蜡炬成灰泪始干 --无题 这里介绍两个比较难的算法: 1.堆排序 2.分治并归排序 先说堆. 这里请大家先自行了解完全二叉树的数据结构. 堆是完全二叉树.大顶堆是在堆中,任意双亲值都大 ...

  8. Java学习笔记——排序算法之希尔排序(Shell Sort)

    落日楼头,断鸿声里,江南游子.把吴钩看了,栏杆拍遍,无人会,登临意. --水龙吟·登建康赏心亭 希尔算法是希尔(D.L.Shell)于1959年提出的一种排序算法.是第一个时间复杂度突破O(n²)的算 ...

  9. 学习笔记——SM2算法原理及实现

    RSA算法的危机在于其存在亚指数算法,对ECC算法而言一般没有亚指数攻击算法 SM2椭圆曲线公钥密码算法:我国自主知识产权的商用密码算法,是ECC(Elliptic Curve Cryptosyste ...

随机推荐

  1. OPENSOURCE - libcurl

    本文仅做备份存档,原文地址如下,请点链接进入 https://www.cnblogs.com/moodlxs/archive/2012/10/15/2724318.html https://www.c ...

  2. 并发编程之java内存模型(Java Memory Model ,JMM)

    一.图例 0.两个概念 Heap(堆):运行时的数据区,由垃圾回收负责,运行时分配内存(所以慢),对象存放在堆上 如果两个线程,同时调用同一个变量,怎两个线程都拥有,该对象的私有拷贝 (可以看一下,T ...

  3. 【非原创】LightOj 1248 - Dice (III)【几何分布+期望】

    学习博客:戳这里 题意:有一个 n 面的骰子,问至少看到所有的面一次的所需 掷骰子 的 次数的期望: 第一个面第一次出现的概率是p1 n/n; 第二个面第一次出现的概率是p2 (n-1)/n; 第三个 ...

  4. Leetcode(15)-三数之和

    给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组. 注意:答案中不可以包含重复的三元组. ...

  5. C# 类 (5)

    Static 静态成员 创建一个类,实例化它,然后使用实例的各种方法或者属性 然而有时候我们不想实例化这个类,难道我们就没法用他里面的成员了吗?比如计算矩形的面积,就是长*宽,万年不变 当然可以,用s ...

  6. 买车交税 All In One

    中国买车交税 All In One 消费税 增值税 车辆购置税 车船税 关税 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问! 原 ...

  7. Linux 如何查看一个文件夹下面有多少个文件

    Linux 如何查看一个文件夹下面有多少个文件 $ tree $ find ./ -type f | wc -l $ ls -l | grep "^-" | wc -l refs ...

  8. codepen iframe theme id

    codepen iframe theme id iframe css theme demos See the Pen css margin collapsing (1. 相邻兄弟元素) by xgqf ...

  9. VIM 官方教程

    VIM 官方教程 zh-hans vim official documents https://www.vim.org/docs.php https://vimhelp.org/ translatio ...

  10. js animation & requestAnimationFrame

    js animation & requestAnimationFrame https://developer.mozilla.org/en-US/docs/Web/API/window/req ...