【数据结构】【平衡树】Treap

匿名 (未验证) 提交于 2019-12-02 22:56:40

百度百科

Pre_knowledge

    二叉排序树

    二叉排序树是一棵二叉树。每个节点对应一个权值v,对于每个节点,一定满足如下性质:

        二叉排序树的每个节点的左子树要么为空,要么左子树的任意节点的权值v‘一定小于v。

        二叉排序树的每个节点的有子树要么为空,要么右子树的任意节点的权值v‘一定大于v。

    二叉排序树的节点:每个节点维护如下信息:该点代表的权值,该点的左右儿子,以该点为根的子树含有元素的个数(不等于子树大小。因为两个相同的元素被认为含有两个元素,但是对应的节点只有一个),该点代表的权值的元素的个数。

    二叉排序树的插入:

       对于要插入的值key,比较他与当前节点的权值v关系,如果v>key,则递归插入左子树,

       否则,如果v==key,则插入当前节点,即++该点元素个数。

       否则,递归插入右子树。

    二叉排序树的删除:

       递归过程同上。- - 该点元素个数。

 

Definition

    Treap=tree+heap,直译树堆。在形态上,他是一颗BST,即二叉排序树。同时,由于二叉排序树的插入方式过于朴素,导致给出具有单调性的数据后会被卡成一条链,任何操作的复杂度从期望O(nlogn)上升到O(n2。为了防止毒瘤出题人造出卡死二叉排序树的数据,我们选择使用魔法打败魔法使用随机化的方法将树的形态按照一定的法则随机转化,防止出现卡出链的情况。

    为了转化树形,我们人为地给每个节点加入一个权值k,强制要求对于每两个有父子关系的节点,父节点的k值小于(大于)子节点。并使用一系列的魔法旋转方式保证树符合这个性质。在旋转的过程中,我们改变了树高,从而改变了整个树的形态。由于父子节点有严格的大小关系,我们说treap具有堆得性质。但是需要指出的是,堆一定是一颗完全二叉树,但treap不满足此性质。

    复杂度:在期望意义下,treap的树高为logn,所以treap的期望时间复杂度为O(nlogn)

    功能:查询一个序列中第k大的数;通过加入、删除维护该序列;查询序列中一个数x的前驱、后继。

    操作:

      1、插入。同二叉排序树。注意每次插入完进行update,同时尝试旋转。

      2、旋转。(注:习惯问题,和江湖上流行的左右旋方向恰好相反)通过左旋和右旋,可以将树转化成满足堆性质的BST。以右旋为例:

        可以看到,右旋时,老根、新根左移。新老根儿子的信息被改变。他们儿子的信息不被改变。

      定理:对于每次递归,最多旋转一次。

        证明:

          由于插入一定会插入到叶节点,在叶节点时显然该子树是一颗合法的树。

          不妨设左右两颗子树均合法,考虑如果出现中间节点不合法的情况,一定是有一侧经过了旋转转换了根。由于不可能同时递归左右两棵子树,所以另一侧的子树一定相对于中间节点合法。在旋转的过程中不难发现合法的一侧其节点相对位置是不变的,即仅交换了不合法的一对的位置,所以只会旋转一次。

        证毕。

       查询:

          序列第K大:如果左子树的size大于k,则递归左子树

                否则如果左子树的size加上根节点的元素个数大于等于k,则返回当前节点的v

                否则递归查找右子树的k-左子树size-根节点size。

      3、删除。 

          删除时,如果该节点对应size>1,则- -size,否则将其权值k赋为INF,通过比较左右子树k的大小决定谁做新的根,经过不断的左右旋转将其沉到叶节点。然后将该节点父节点的指针清空。这样就完成了删除。在数组版的treap中,这么做会产生内存泄漏问题,不过问题不大(逃)如有需要可以手写内存池。

      4、前驱、后继。

          一个数x的前驱定义为小于这个数的最大的数。递归整棵树,递归过程如下:

            如果当前节点为空,返回—INF。

            否则,如果当前节点的v<x 则返回max(v,递归当前节点的右子树)

            否则返回当前节点左子树的递归值。

          后继同理。

Sample

【lgP3369】 普通平衡树

Description

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 x 数
  2. 删除 x 数(若有多个相同的数,因只删除一个)
  3. 查询 x 数的排名(排名定义为比当前数小的数的个数 +1 。若有多个相同的数,因输出最小的排名)
  4. 查询排名为 x 的数
  5. 求 x 的前驱(前驱定义为小于 x ,且最大的数)
  6. 求 x 的后继(后继定义为大于 x ,且最小的数)

Input

第一行为 n ,表示操作的个数,下面 nn 行每行有两个数 opt 和 x , opt 表示操作的序号( 1opt6 )

Output

对于操作 3,4,5,63,4,5,6 每行输出一个数,表示对应答案

Sample Input

10 1 106465 4 1 1 317721 1 460929 1 644985 1 84185 1 89851 6 81968 1 492737 5 493598

Sample Output

106465 84185 492737

Hint

1.n的数据范围:n100000

2.每个数的数据范围:[107,107]

Solution

模板题。代码拆分如下:

1、定义:

struct BST {     int v,k,ls,rs,sm,sz,fa;     BST() {v=-INF;k=INF;ls=rs=fa=-1;sm=sz=0;} }; BST treap[maxn];int tsz;

其中,v代表键值,k代表权值(即随机给定的值),ls、rs为左右儿子的下标,sm=same即键值为v的元素个数。sz=size为以该节点为根的子树大小。fa♂为父节点。构造函数上,注意初始化sm=sz=0可以保证程序鲁棒。tsz=totalsize,代表当前用到的最大数组下标。

2、声明:

void T_begin(); void lftun(int); void rtun(int); void turnning(int); void rememory(int); void buildnew(int,int); void addthis(int); void dltthis(int); void build(int,int); void add(int,int); void dlt(int,int); int rak(int,int); int ask_rak(int,int); int ask_upper(int,int); int ask_lower(int,int);

只是给你体验一下他的恐怖

3、初始化

  添加一个键值超级大权值超级小的点作为超级根,所有的函数都将从超级根开始向下递归。真正的BST是超级根的左子树。

inline void T_begin() {     treap[0].v=INF;treap[0].k=-INF;treap[0].sz=treap[0].sm=1; }

4、添加点

核心函数:

void add(int now,int v) {     BST &t=treap[now];     if(t.v==-INF) {buildnew(now,v);return;}     
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!