最简单的二叉可并堆——斜堆
SuperJvRuo
2018-07-23 15:16:43
要求:了解什么是可并堆,什么是斜堆。不必熟练掌握斜堆的写法。
## 一、什么是可并堆
一般的二叉堆支持在$\log n$时间内完成以下操作:
1. 插入一个值
2. 获得或取出最小值(或最大值)。
这样的二叉堆想必大家早已经能熟练地用```std::priority_queue```水过了。
可并堆还要求在$\log n$的时间内完成两个堆的合并。原来的二叉堆要想完成这一操作,时间复杂度为$n\log n$,使用启发式合并的话,比我们想要的复杂度足足多了一个$\log$,这是不清真的。
为了实现这一操作,我们可以使用斜堆、左偏树、随机堆或配对堆~~再或者pb_ds中的各种优先队列~~。
## 二、斜堆的整体结构
斜堆是一棵二叉树,满足堆的性质。以小根堆为例,每个非根结点的值都比它父亲大。因此在整棵斜堆中,根的值最小。
原来我们写过的二叉堆是一棵完全二叉树,但斜堆不是。斜堆甚至不必保证平衡,每个结点的左右儿子的大小关系也没有任何规定。
对于每个节点,我们这样描述:
```
struct Node
{
int ch[2],val;
}
```
也就是记录自己的值和左右儿子。
## 三、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;
}
```
## 四、pop
删除根节点,合并两个子树即可。
## 五、时间复杂度分析
最坏情况下,斜堆可以被精心构造的毒瘤数据卡成一条链,时间复杂度$O(n)$。然而,在绝大多数情况下,均摊时间复杂度是$O(\log n)$,有人证明,其均摊复杂度最坏不超过$O(4\log n)$。
同时,有这样一个问题,如果合并后的左子树仍远大于右子树,交换后再进行后续合并不就更偏了吗?
## 六、例题
[P2475 SCOI2008 斜堆](https://www.luogu.org/problemnew/show/P2475)