前言

可合并的堆有二叉堆、二项堆、配对堆、斐波那契堆等,光是二叉堆的合并就有无脑启发式合并、左偏树、斜堆等做法。

这里主要讲的是二叉堆,想了解其它优秀的可并堆可以看看DSCN上优秀的博客。

启发式合并

使用优先队列,每次把较小的堆的点一一取出来合并到大的堆中,复杂度均摊 O(log⁡2n)O(\log^2 n)O(log2n)。

优点:思路简单清晰,非常好实现。

缺点:主要是太慢了。其次是只允许普通的合并和弹出,搞其它奇怪的操作可能会破坏均摊复杂度。

代码很简单就不水了。

左偏树

最标准常用的做法,复杂度非常优秀。

对普通二叉堆(不平衡)的每个节点定义一个“距离”:如果右儿子为空则距离为1,否则距离为右儿子距离+1。

一棵左偏树满足以下条件(左偏性):每个节点的右儿子距离不大于左儿子距离。这样一来,一棵 nnn 个节点的左偏树的距离最大为 ⌊log⁡(n+1)−1⌋\lfloor\log (n+1)-1\rfloor⌊log(n+1)−1⌋。

合并操作时,每次将优先级低的根节点与优先级高的根节点的右儿子合并,递归下去,回溯时判断是否交换左右儿子维护左偏性,以及更新距离即可。由于操作次数只与右儿子方向的深度,也就是“距离”有关,所以单次复杂度最大 O(log⁡n)O(\log n)O(logn)。

核心代码——合并:

inline bool cmp(int a,int b){return a>b;}//设置比较函数
inline void update(int x){  //维护左偏性if(t[t[x].sn[0]].dis<t[t[x].sn[1]].dis)swap(t[x].sn[0],t[x].sn[1]);t[x].dis=t[t[x].sn[1]].dis+1;
}
inline int merg(int x,int y){if(!x||!y)return x^y;if(cmp(t[x].val,t[y].val))swap(x,y);t[x].sn[1]=merg(t[x].sn[1],y);update(x);return x;
}

插入节点就把它当单个的堆合并即可。

删除节点时,如果节点是根,就直接把左右子树合并即可。否则比较麻烦,需要把左右子树合并后,接在父亲节点上,然后往上更新维护左偏性。

注意,左偏树不保证平衡,所以最大深度大于 log 级,从一个点往上遍历到根会超时。但是我们可以加一个优化:从一个点往上更新不动时,直接退出。这时往上遍历的长度不超过最大的 dis,所以删点的总复杂度也是 O(log⁡n)O(\log n)O(logn)。

由于每次操作都是严格 O(log⁡n)O(\log n)O(logn),所以可以进行可持久化。

斜堆

合并操作和左偏树非常相似,然而比左偏树还要简单。每次合并到右儿子后,直接无脑交换左右儿子即可。

inline bool cmp(int a,int b){return a>b;}
inline int merg(int x,int y){if(!x||!y)return x^y;if(cmp(t[x].val,t[y].val))swap(x,y);t[x].sn[1]=merg(t[x].sn[1],y);swap(t[x].sn[0],t[x].sn[1]);return x;
}

赞美太阳! 这时间不会被卡吗?

不会。这个合并的复杂度是均摊 O(nlog⁡n)O(n\log n)O(nlogn) 的。复杂度分析可以看这里。

合并的复杂度是对的,删除根节点的复杂度是对的,但是我不知道删除任意点的时候是否可以直接删。

另外,这个复杂度是均摊的,所以如果整一些奇怪操作可能会破坏均摊复杂度(比如可持久化,重复某一个历史操作),所以斜堆 应 该 是不能可持久化的。

我自己yy的一个可并堆的打法。

我们知道普通的暴力堆合并最坏是 O(n)O(n)O(n) 的,因为如果一直往左儿子或右儿子合并可能会出现很长的一条链。

回忆一下平衡树是怎么解决这个问题的:随机!

具体地,我们每次合并两个根时,如果在上面的根有一个儿子是空的,那么就合并到空的儿子那儿,否则随机往左儿子或右儿子方向合并。

inline bool cmp(ll a,ll b){return a>b;}
inline int mergh(int x,int y){if(!x||!y)return x^y;if(cmp(t[x].val,t[y].val))swap(x,y);bool o=rand()&1;if(!t[x].sn[o^1])o^=1;t[x].sn[o]=mergh(t[x].sn[o],y);return x;
}

随机过后,给普通堆合并带来哪些变化呢?经过测试,堆的最大深度的期望并没有达到期望的 log⁡n\log nlogn 级别,但是合并、插入、删除操作单次的期望复杂度都是 O(log⁡n)O(\log n)O(logn) 的。

就拿合并来说,由于算法遇到儿子数<2的节点就直接合并一次返回了,所以主要都是在左右儿子不为空的节点上遍历。随机访问情况下,这种节点显然最多遍历 O(log⁡n)O(\log n)O(logn) 个。

删除任意节点的时候,只需要合并左右子树然后接到父亲上即可,不需要像左偏树那样往上维护什么左偏性。因为通过上面的分析我们知道,合并的复杂度与树的形态无关。

另外,随堆也可以实现可持久化。由于每次合并复杂度是均匀的,不会出现斜堆的那种均摊被卡的情况。而且由于不维护距离、不交换左右子树,它的可持久化 应 该 比左偏树好打一些。

附上可持久化的代码(仅多了1行):

inline bool cmp(ll a,ll b){return a>b;}
inline int mergh(int x,int y){if(!x||!y)return x^y;if(cmp(t[x].val,t[y].val))swap(x,y);int o=rand()&1;if(!t[x].sn[o^1])o^=1;int res=++IN;t[res]=t[x];t[res].sn[o]=mergh(t[x].sn[o],y);return res;
}

可见,除了常数大一点,随堆其实和左偏树一样万能。

update2021.10.4:

最近学了一下C++11里面的新随机数,发现同为 O(1)O(1)O(1) 随机数,新旧常数的差别却非常大。因为这个随堆合并每次都要调用随机数,非常依赖它的速度,所以应该根据情况选用不同随机方法。

如果运行不开O2,那么最好用上面的经典rand随机;

开了O2的话,用梅森旋转的随机数引擎可以快10~20倍:
(注意:不开O2的梅森旋转会比rand慢很多)

inline bool cmp(ll a,ll b){return a>b;}
mt19937 Rand(*new(int));
inline int mergh(int x,int y){if(!x||!y)return x^y;if(cmp(t[x].val,t[y].val))swap(x,y);int o=Rand()&1;if(!t[x].sn[o^1])o^=1;t[x].sn[o]=mergh(t[x].sn[o],y);return x;
}

顺便提一下:以time(0)作为随机数种子是最好的,但是在有的比赛或网站上面却是禁用的,
CCF官方曾经也声明过禁止使用这个,后来好像没有了限制,但又没有声明可以使用。所以在比赛和做题中用time(0)是有风险的。

怎么办呢?HandInDevil给出了祂的**大法:用新声明的整形变量的地址(*new(int))作为随机的种子,同样可以使每次运行程序得到不同的结果!(好像只有Windows下管用)

C++ pb_ds库 binary heap tag

好家伙,没想到C++11已经有现成的可并堆了。学NM直接用

详细介绍可以看于纪平的《C++的pb_ds库在OI中的应用》:




总结

其实手写的堆合并并不复杂,而且总是比封装的数据结构灵活一些。

反正我大多数情况都用的随堆。

浅谈几种常用二叉堆合并相关推荐

  1. 很全!浅谈几种常用负载均衡架构

    阅读本文大概需要 9 分钟. 作者:Kingreatwill 链接:http://t.cn/Ea8JcrS 什么是负载均衡(Load balancing) 在网站创立初期,我们一般都使用单台机器对台提 ...

  2. 浅谈五种常用的特征选择方法

  3. ReviewForJob——二叉堆优先队列的实现(三种堆节点类型——int + struct HeapNode + struct HeapNode*)

    [0]README 1)本文旨在给出 二叉堆优先队列的实现 的代码实现和分析, 而堆节点类型 不外乎三种: 一, 基本类型如int: 二,结构体类型 struct HeapNode: 三,结构体指针类 ...

  4. 优先队列与相关题目(Python、二叉堆)

    1. 优先队列知识 1.1 优先队列简介 优先队列:一种特殊的队列.在优先队列中,元素被赋予优先级,当访问队列元素时,具有最高优先级的元素最先删除. 优先队列与普通队列最大的不同点在于出队顺序 普通队 ...

  5. 数据结构之优先队列--二叉堆(Java实现)

    前言 数据结构队列的学习中,我们知道队列是先进先出的.任务被提交到队列中,按照先进先出的原则 对各个任务进行处理.不过在现实的情况下,任务通常有着优先级的概念,例如短任务.管理员的操作 应该优先执行. ...

  6. python优先队列的库,python优先队列及二叉堆的实现

    python优先队列及二叉堆的实现 发布于 2015-12-18 06:55:17 | 117 次阅读 | 评论: 0 | 来源: PHPERZ Python编程语言Python 是一种面向对象.解释 ...

  7. java实现二叉堆,数据结构基础篇-二叉堆

    二叉堆分为两种,最大堆和最小堆,我们只讨论最小堆的性质,最大堆具有相同的原理. 最小堆是一种符合下面两个特性的树形结构: 最小堆是一颗完全二叉树,即最小堆的每个节点要么没有子节点,要么只有一个左子节点 ...

  8. 二叉堆简单实现与应用

    从二叉树谈二叉堆: 二叉树可以简单认为是父节点最多由左.右两个子节点组成的树,常用的二叉树有完全二叉树.二叉搜索树.二叉搜索树在极端境况下存在"跛脚"的问题,由此又有了二叉平衡树. ...

  9. A星寻路与二叉堆优化(2D)

    内容概览 前言 吹水 什么是A星寻路 如何实现A星寻路 节点类 网格类 寻路 优化 使用 使用演示 Hireachy窗口内配置 player配置 GridOwner配置 最终效果 前言 Hi~你好,我 ...

最新文章

  1. Python使用sklearn构建广义线性模型:泊松回归(Poisson regression)实战
  2. mysql非聚集索引区间查询_mysql的聚集索引和非聚集索引,回表查询,索引覆盖,最左前缀原则略解...
  3. Elasticsearch 2.2.0 索引配置详解
  4. [转]不定义JQuery插件,不要说会JQuery
  5. 广西中专机器人应用与维护_我校2018级工业机器人应用与维护专业跟岗实习
  6. 【CKFinder】解决上传中文名文件乱码和文件重命名的问题
  7. 4690s i5_秒杀i7?小恶魔i5-4690K对决i7-4770K
  8. 基站建设(三元环计数+根号分治 / bitset)
  9. LeetCode:204. 计数质数
  10. C++远征之封装篇——常对象成员、常成员函数
  11. C语言——指针篇(四)多维数组和多维指针(内含数组指针和指针数组笔记)
  12. Pyqt之模态与无模态对话框(Modal and Modeless)
  13. “华为杯”——中国研究生数学建模大赛相关解读及LaTeX模版、算法、真题、优秀论文等相关资源分享(超详细)
  14. 基于Go的马蜂窝旅游网分布式IM系统技术实践
  15. 教学小结:我这样帮助学生提出疑问
  16. Mysql从入门到入魔——6. 表联结、组合查询
  17. jquery 源码分析系列1
  18. Hadoop技术优缺点详解
  19. oracle中授予connect权限,oracle授予权限
  20. 基于自适应权重和Levy飞行的改进鲸鱼优化算法

热门文章

  1. 将爱心代码设为电脑屏保,俘获少女芳心,还能假装黑客,在酷炫的界面中保护隐私
  2. 英语口语练习系列-C21-美式幽默
  3. 哎!2019年最后1天,我从外包公司辞职了...
  4. 计算机巧用剪纸做画册教案,手工制作教案:剪纸教学案例
  5. VS2010通过Architecture创建UML用例图设计
  6. Java程序员必经的实践之路:java控制台在哪里打开
  7. 发布订阅模式与观察者模式
  8. Android开发中apk开启sdcard的读写权限
  9. 我的世界服务器精英怪修改,我的世界稀有精英怪
  10. android极光推送 小米,android 接极光推送厂商通道,华为 小米 VIVO OPPO