算法详解——左偏树

可并堆是啥

给你两个优先队列,要你合并这两个优先队列,而且还要保证合并后符合优先队列的性质,时间复杂度要限制在 log ⁡ ( s 1 + s 2 ) \log (s_1+s_2) log(s1+s2)之内。

怎么做?现在普通的堆已经满足不了要求了。

这个时候,就要用一种神奇的数据结构:左偏树了。

(二项堆,斐波那契堆不在本文的考虑范围内)

左偏树

顾名思义,左偏树就是一棵向左偏的树(逃

我们定义一个节点为外节点,当且仅当这个节点的左子树和右子树中的一个是空节点。(注意外节点不是叶子节点)

一个点的距离,被定义为它子树中离他最近的外节点到这个节点的距离(这与树的深度不同)

(图片来源:百度百科)

如图,蓝色的数字就是距离。

我们规定,空节点的距离为 − 1 -1 1

一个合格的左偏树,必须满足下列性质:

  • 性质1. 左偏树中,任何一个节点的父节点的权值都要小于等于这个节点的权值(堆性质)

    v a l f a i < v a l i val_{fa_i}<val_i valfai<vali

    这告诉我们:左偏树很像堆,如果长成了完全二叉树,那就是一个堆。

  • 性质2. 左偏树中任意一个节点的左儿子的距离一定大于等于右儿子的距离(左偏性质)

    d i s t l s i ≥ d i s t r s i dist_{ls_i}\ge dist_{rs_i} distlsidistrsi

    需要注意的一点:左偏指的不是左右儿子的大小,因此如果左儿子是一个点,而右儿子是一条很长的链,那也是满足要求的左偏树。

  • 推论1. 左偏树中任意一个节点的距离为其右儿子的距离 + 1 +1 +1

    d i s t i = d i s t r s i + 1 dist_i=dist_{rs_i}+1 disti=distrsi+1

  • 推论2. n n n个点的左偏树,距离最大为 log ⁡ ( n + 1 ) − 1 \log (n+1)-1 log(n+1)1

    这个怎么证明呢?考虑左偏树根节点的距离 d d d为一定值,那么节点数最少的情况就是一个完全二叉树,节点数为 2 d + 1 − 1 2^{d+1}-1 2d+11。那么 n n n个节点的左偏树距离也就 ≤ log ⁡ ( n + 1 ) − 1 \leq \log(n+1)-1 log(n+1)1

    这个推论成为了左偏树时间复杂度的保证。

利用上面的推论,就有了左偏树的基础操作:

merge!

对,就是合并,在堆中最难完成的操作,变成了左偏树的最基础操作。

现在有两棵左偏树, a , b a,b a,b是他们的根节点,假设 v a l a ≤ v a l b val_a\leq val_b valavalb(否则swap一下 a , b a,b a,b

既然 v a l a ≤ v a l b val_a\leq val_b valavalb,说明如果将这两棵树合并,根应该还是 a a a

这时,只需要递归合并 r s a , b rs_a,b rsa,b这两个点,并将新树的根节点作为 r s a rs_a rsa

合并完成之后, d i s t r s a dist_{rs_a} distrsa可能会变,为了保证左偏性质,如果 d i s t l s a < d i s t r s a dist_{ls_a}<dist_{rs_a} distlsa<distrsa,就交换 a a a的左右儿子。

最后,更新 d i s t a dist_a dista,以 a a a为合并后的树的根节点。

哇,好简单!

(图片来源:洛谷)

具体流程如图所示。

时间复杂度?可以看出,每次 d i s t a + d i s t b dist_a+dist_b dista+distb都至少减少了 1 1 1(推论1),最坏情况会减少到 − 1 -1 1,由推论2, d i s t a + d i s t b = log ⁡ ( s i z e a + 1 ) + log ⁡ ( s i z e b + 1 ) − 1 dist_a+dist_b=\log(size_a+1)+\log(size_b+1)-1 dista+distb=log(sizea+1)+log(sizeb+1)1,因此时间复杂度为 O ( log ⁡ s i z e a + log ⁡ s i z e b ) O(\log size_a+\log size_b) O(logsizea+logsizeb)

push!

push其实就是一个只有一个节点的左偏树与一棵有很多节点的左偏树合并。

时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

pop!

将根的左右儿子合并,然后将新合并的树的父亲设为NULL。

时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)

build!

给数组 a i a_i ai建一棵左偏树。

暴力插入时间复杂度 O ( ∑ i = 1 n log ⁡ n ) O(\sum_{i=1}^n \log n) O(i=1nlogn)。(这是多少啊,我不会啊,大佬教教我)

upd:这个是 O ( n log ⁡ n ) O(n\log n) O(nlogn),但是常数比较小。

考虑像建普通堆一样建左偏树,建一个队列,开始是 n n n个单独的节点,每次取出队列头两个节点,merge起来,丢到队列尾去。时间复杂度 O ( n ) O(n) O(n)

但是,找到一个节点所在的左偏树的根,最坏情况是 O ( n ) O(n) O(n)的(建成一条链)。所以说,如果有哪个丧心病狂的出题人给了一组这样的数据,那您的左偏树就GG了,可以考虑换一个数据结构。

代码实现

以洛谷模板题为例:

题目链接:https://www.luogu.org/problemnew/show/P3377

代码:https://paste.ubuntu.com/p/ZpjCG6cmJT/

使用平板电视库

参考WC2015营员交流《C++的pb_ds库在OI中的应用》

你需要一个头文件ext/pb_ds/priority_queue.hpp;

你需要一个namespace __gnu_pbds;

然后你就可以直接使用

priority_queue<T>

pbds默认调用配对堆。使用a.join(b)就可以将 a , b a,b a,b合并在一起了~

当然,可以通过改模板参数,调用二项堆,斐波那契堆,二叉堆(普通的堆)等等,但是配对堆更快,所以使用默认就可以了。

其他的操作都与STL堆相同。

你可能感兴趣的:(左偏树,算法模板)