几种初级二叉可并堆简介

SuperJvRuo

2018-07-24 08:58:50

Personal

## 前言 我问过一些男性OIer,女生的哪些习惯会让你厌恶? 有人说:不温柔。 有人说:分块时不解均值不等式,默认块的大小为$\sqrt{n}$。 有人说:写Treap、模拟退火时用系统默认随机数种子。 有人说:利用CRT解一元模线性方程组时忽略不互质的情况直接套用公式。 还有人说:偷懒使用斜堆代替左偏树。 emm……斜堆,左偏树…… 前置知识:对[洛谷日报第11期](https://www.luogu.org/blog/henry-y/qian-xi-ji-chu-shuo-ju-jie-gou-er-cha-dui)中的二叉堆完全理解,且可以完全实现。 ## 一、可并堆 一般的二叉堆支持完成以下操作: 1. $O(\log n)$插入一个值 2. $O(1)$获得或$O(\log n)$取出最小值(或最大值)。 这样的二叉堆想必大家早已经能熟练地用```std::priority_queue```水过了。 可并堆还要求在$\log n$的时间内完成两个堆的合并。原来的二叉堆要想完成这一操作,时间复杂度为$n\log n$,使用启发式合并的话,比我们想要的复杂度足足多了一个$\log$,这是不清真的。 为了实现这一操作,我们可以使用斜堆、左偏树、随机堆、配对堆、二项堆或斐波那契堆~~再或者```pb_ds```中的各种优先队列~~。 这里讲三种容易理解,容易实现的可并堆。 ## 二、斜堆 ### 1、斜堆的整体结构 斜堆是一棵二叉树,满足堆的性质。以小根堆为例,每个非根结点的值都比它父亲大。因此在整棵斜堆中,根的值最小。 原来我们写过的二叉堆是一棵完全二叉树,但斜堆不是。斜堆甚至不必保证平衡,每个结点的左右儿子的大小关系也没有任何规定。 对于每个节点,我们这样描述: ``` struct Node { int ch[2],val; } ``` 也就是记录自己的值和左右儿子。 ### 2、push和merge 我们可以把单独的一个节点也看成是一个斜堆,这样可以统一为```merge```操作。 ![](https://cdn.luogu.com.cn/upload/pic/24924.png ) 一开始我们可能会想到这样的搞法:选择a和b两个斜堆中根节点键值较小的一个,让它的右子树与另一棵树递归合并下去,再把合并出的东西当做新的右子树。 但是这样下去,右子树会很dark♂,左子树却很小,斜堆变得不平衡。 ![](https://cdn.luogu.com.cn/upload/pic/24927.png) 我们可以再加一步操作,插入完就交换左右儿子。这样,在一次次的合并后,这个斜堆就较为平衡了。 ![](https://cdn.luogu.com.cn/upload/pic/24931.png ) ![](https://cdn.luogu.com.cn/upload/pic/24932.png ) 以上操作也可以换成插入到左子树。 ``` int merge(int root1,int root2) { if(!root1||!root2) return root1+root2; if(node[root1].val>node[root2].val) std::swap(root1,root2); node[root1].ch[1]=join(node[root1].ch[1],root2); std::swap(node[root1].ch[0],node[root2].ch[1]); return root1; } ``` ### 3、pop 删除根节点,合并根节点的两个子树即可。 ### 4、时间复杂度分析 最坏情况下,斜堆可以被精心构造的毒瘤数据卡成一条链,时间复杂度$O(n)$。然而,在绝大多数情况下,均摊时间复杂度是$O(\log n)$,有人证明,其均摊复杂度最坏不超过$O(4\log n)$。 同时,有这样一个问题,如果合并后的左子树仍远大于右子树,交换后再进行后续合并不就更偏了吗? ## 二、左偏树 ### 1、左偏树的结构与性质 左偏树也是一棵二叉树,也满足堆的性质。以小根堆为例,每个非根结点的值都比它父亲大。因此在整棵左偏树中,根的值最小。 即左偏树的性质一(以小根堆为例): $$val_p\leq val_{lson}$$ $$val_p\leq val_{rson}$$ 左偏树的每个节点记录四个值:左儿子,右儿子,权值和高度。 ``` struct Node { int ch[2],val,height; } ``` 所谓高度,是指一个节点到其子树内最近叶节点的距离。叶节点的高度是$0$。 为了体现名字中的“左偏”,我们钦定每个节点左儿子的高度大于等于右儿子的高度。整个树就是一种左边重右边轻的状态。 即左偏树性质二: $$height_{lson}\leq height_{rson}$$ 所以,距离父节点最近的叶节点一定在右子树中,可得左偏树性质三: $$height_p=height_{rson}+1$$ 节点数一定的情况下,要使根节点的高度最大,就要让这棵树形成完全二叉树。这样可以使得离根节点最近的叶节点尽可能远。 设此时$height_{root}=k$,则有节点数$n=2^{k+1}-1$。即得所有情况下$n\geq 2^{k+1}-1$。易见左偏树性质四: $$height_{root}\leq \log^{n+1}_2-1$$ ### 2、push和merge 同样地,把```push```视为```merge```来处理。 当我们要合并两棵左偏树a和b时,不妨设$val_{roota}<val_{rootb}$。大部分操作和斜堆相同,不同的是要保证左偏性质,还要维护节点的距离。那么只需在合并后检查两个儿子的高度大小关系,当左儿子高度小于右儿子时交换即可。在返回新得到的树根前,修改根节点的高度,使其满足性质三。 合并前 ![](https://cdn.luogu.com.cn/upload/pic/24931.png) 合并后 ![](https://cdn.luogu.com.cn/upload/pic/24936.png) ``` int merge(int root1,int root2) { if(!root1||!root2) return root1+root2; if(node[root1].val>node[root2].val) std::swap(root1,root2); node[root1].ch[1]=merge(node[root1].ch[1],root2); if(node[node[root1].ch[0]].height>node[node[root1].ch[1]].height) std::swap(node[root1].ch[0],node[root1].ch[1]); node[root1].height=node[node[root1].ch[1]].height+1; return root1; } ``` ### 3、pop 参考斜堆。 ### 4、时间复杂度分析 左偏树的左偏性保证了它不会退化成链,保证了它最坏情况下的效率。由于两棵被合并的左偏树a和b的根节点高度分别不大于$\log^{size_a+1}_2-1$和$\log^{size_b+1}_2-1$,合并的复杂度为$\log n$。 然而可以从~~我写的很丑的~~代码看出,左偏树的常数比斜堆要大,在实际应用中,面对随机数据,反而比斜堆要慢。 ## 三、随机堆 ### 1、随机堆的结构 参见斜堆。 ### 2、push和merge ```push```视为```merge```。 为了减小常数与略微减少代码量,我们必须舍弃左偏树的交换与维护。但是又不能像斜堆那样被卡成一条链。也就是说,我们要往两边都均匀插入。怎么办呢? 随机数了解一下! ``` int merge(int root1,int root2) { if(!root1||!root2) return root1+root2; if(node[root1].val>node[root2].val) std::swap(root1,root2); int opt=rand()&1; node[root1].ch[opt]=merge(node[root1].ch[opt],root2); return root1; } ``` 这玩意甚至不需要交换左右儿子!而且毒瘤出题人没法卡它! ### 3、pop 参考斜堆。 ### 4、时间复杂度分析 合并操作最坏$O(n)$,均摊$O(\log n)$。常数小,代码复杂度小~~(说得好像左偏树代码复杂似的)~~,可以完全替代左偏树。 ## 四、总结 以后不需要偷懒用斜堆代替左偏树了,我们可以洗把脸然后用随机堆。~~当然```pb_ds```也是很好的选择。~~ 留三道练习题: [P3377 【模板】左偏树(可并堆)](https://www.luogu.org/problemnew/show/P3377) 这道题的并查集不能路径压缩。 [P1456 Monkey King](https://www.luogu.org/problemnew/show/P1456) 这道题是简单题。 [P3261 JLOI2015 城池攻占](https://www.luogu.org/problemnew/show/P3261) 这道题不止可并堆这一种做法,但是可并堆的代码复杂度最低。 (逃