zoukankan      html  css  js  c++  java
  • 平衡树——treap

    Treap 简介

      Treap 是一种二叉查找树。它的结构同时满足二叉查找树(Tree)与堆(Heap)的性质,因此得名。Treap的原理是为每一个节点赋一个随机值使其满足堆的性质,保证了树高期望 O(log2n) ,从而保证了时间复杂度。 
      Treap 是一种高效的平衡树算法,在常数大小与代码复杂度上好于 Splay。

    Treap 的基本操作

      现在以 BZOJ 3224 普通平衡树为模板题,详细讨论 Treap 的基本操作。

    1.基本结构

      在一般情况下,Treap 的节点需要存储它的左右儿子,子树大小,节点中相同元素的数量(如果没有可以默认为1),自身信息及随机数的值

    1 struct sd{
    2     int l,r,sz,key,rd,re;
    3 }t[100005];

    其中 l 为左儿子节点编号, r 为右儿子节点编号, key 为节点数值, sz 为子树大小, rd 为节点的随机值, re为该节点数值的出现次数(目的为将所有数值相同的点合为一个)。

    2.关于随机值

      随机值由 rand() 函数生成, 考虑到 <cstdlib> 库中的 rand() 速度较慢,所以在卡常数的时候建议手写 rand() 函数。

    1 inline int rand(){
    2     static int seed = 2333;
    3     return seed = (int)((((seed ^ 998244353) + 19260817ll) * 19890604ll) % 1000000007);
    4 }

    其中 seed 为随机种子,可以随便填写。

    3.节点信息更新

      节点信息更新由 update() 函数实现。在每次产生节点关系的修改后,需要更新节点信息(最基本的子树大小,以及你要维护的其他内容)。 
      时间复杂度 O(1) 。

    1 inline void update(int k){
    2     t[k].sz = t[l].sz + t[r].sz + t[k].re;
    3 }

    4.「重要」左旋与右旋

      左旋与右旋是 Treap 的核心操作,也是 Treap 动态保持树的深度的关键,其目的为维护 Treap 堆的性质。 
      下面的图片可以让你更好的理解左旋与右旋:

      这里写图片描述

      下面具体介绍左旋与右旋操作。左旋与右旋均为变更操作节点与其两个儿子的相对位置的操作。 
      「左旋」为将作儿子节点代替根节点的位置, 根节点相应的成为左儿子节点的右儿子(满足二叉搜索树的性质)。相应的,之前左儿子节点的右儿子应转移至之前根节点的左儿子。此时,只有之前的根节点与左儿子节点的 sz 发生了变化。所以要 update() 这两个节点。 
      「右旋」类似于「左旋」,将左右关系相反即可。 
      时间复杂度 O(1) 。 

     1 void right(int &k)
     2 {
     3     int y=t[k].l;t[k].l=t[y].r;t[y].r=k;
     4     t[y].sz=t[k].sz;
     5     update(k);k=y;
     6 }
     7 void left(int &k)
     8 {
     9     int y=t[k].r;t[k].r=t[y].l;t[y].l=k;
    10     t[y].sz=t[k].sz;
    11     update(k);k=y;
    12 }

    5.节点的插入与删除

      节点的插入与删除是 Treap 的基本功能之一。 
      「节点的插入」是一个递归的过程,我们从根节点开始,逐个判断当前节点的值与插入值的大小关系。如果插入值小于当前节点值,则递归至左儿子;大于则递归至右儿子;

      相等则直接在把当前节点数值的出现次数 +1 ,跳出循环即可。如果当前访问到了一个空节点,则初始化新节点,将其加入到 Treap 的当前位置。 
      「节点的删除」同样是一个递归的过程,不过需要讨论多种情况: 
      如果插入值小于当前节点值,则递归至左儿子;大于则递归至右儿子。 
      如果插入值等于当前节点值: 
        若当前节点数值的出现次数大于 1 ,则减一; 
        若当前节点数值的出现次数等于于 1 : 
          若当前节点没有左儿子与右儿子,则直接删除该节点(置 0); 
          若当前节点没有左儿子或右儿子,则将左儿子或右儿子替代该节点; 
          若当前节点有左儿子与右儿子,则不断旋转 当前节点,并走到当前节点新的对应位置,直到没有左儿子或右儿子为止。 
      时间复杂度均为 O(log2n) 。 
      具体实现代码如下:

     1 void inin(int &k,int x)
     2 {
     3     if(k==0)
     4     {
     5         size++;
     6         k=size;t[k].sz=1;
     7         t[k].re=1;
     8         t[k].key=x;
     9         t[k].rd=rand(); 
    10         return;
    11     }
    12     t[k].sz++;
    13     if(t[k].key==x) 
    14     t[k].re++;
    15     else
    16     {
    17         if(x>t[k].key)
    18         {
    19             inin(t[k].r,x);
    20             if(t[t[k].r].rd<t[k].rd) 
    21             left(k);
    22         }
    23         else
    24         {
    25             inin(t[k].l,x);
    26             if(t[t[k].l].rd<t[k].rd)
    27             right(k);
    28         }
    29     }
    30 }
    31 void del(int &k,int x)
    32 {
    33     if(k==0)
    34     return;
    35     if(t[k].key==x)
    36     {
    37         if(t[k].re>1)
    38         {
    39             t[k].re--;
    40             t[k].sz--;
    41             return;
    42         }
    43         if(t[k].l*t[k].r==0)
    44         k=t[k].l+t[k].r;
    45         else
    46         {
    47             if(t[t[k].l].rd<t[t[k].r].rd)
    48             right(k),del(k,x);
    49             else
    50             left(k),del(k,x);
    51         }
    52     }
    53     else
    54     {
    55         if(x>t[k].key)
    56         {
    57             t[k].sz--;
    58             del(t[k].r,x);
    59         }
    60         else
    61         {
    62             t[k].sz--;
    63             del(t[k].l,x);
    64         }
    65     }
    66 }

    接下来来一道treap模板题,具体其它操作可在代码中学习,有较详细注释

    新手代码可能有点冗长,作为蒟蒻,希望大佬勿喷。

    https://www.luogu.org/problemnew/show/P3369

    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    #include<ctime>
    using namespace std;
    struct sd{
        int l,r,sz,key,rd,re;//树的左,右,大小,关键值,随机权值,重复次数
        //我这里建立的是小根堆,即随机权值小的在上方 
    }t[100005];
    int size,ans,root;
    void update(int k)//每次上浮都要更新树的大小 
    {
        t[k].sz=t[t[k].l].sz+t[t[k].r].sz+t[k].re;
    }
    void right(int &k)//向右旋转,是左子树就右旋 
    {
        int y=t[k].l;t[k].l=t[y].r;t[y].r=k;
        t[y].sz=t[k].sz;
        update(k);k=y;
    }
    void left(int &k)//向左旋转 ,是右子树就左旋 
    {
        int y=t[k].r;t[k].r=t[y].l;t[y].l=k;
        t[y].sz=t[k].sz;
        update(k);k=y;
    }
    void inin(int &k,int x)//插入x
    {
        if(k==0)//判断是否到了叶节点,如果是就开始插入X 
        {
            size++;
            k=size;t[k].sz=1;
            t[k].re=1;
            t[k].key=x;
            t[k].rd=rand();//随机权值,保证平衡树的随机性与唯一性,让出题人卡不了 
            return;
        }
        t[k].sz++;//每次向下插入时都要在子树大小加一
        if(t[k].key==x)//如果要插入的数原本就存在,那就直接在这个结点数的重复次数+1. 
        t[k].re++;
        else
        {
            if(x>t[k].key)
            {
                inin(t[k].r,x);//到右子树中去找 
                if(t[t[k].r].rd<t[k].rd)//每次插入后判断是否改变了平衡树堆的性质 
                left(k);
            }
            else
            {
                inin(t[k].l,x);//在左子树中找 
                if(t[t[k].l].rd<t[k].rd)
                right(k);
            }
        }
    }
    void del(int &k,int x)//删除x
    {
        if(k==0)
        return;
        if(t[k].key==x)//找到了目标x就将其下沉 
        {
            if(t[k].re>1)//如果x重复多次出现,只用删除一个,那就不用下沉了,直接将重复次数-1 
            {
                t[k].re--;
                t[k].sz--;
                return;
            }
            if(t[k].l*t[k].r==0)//如果某个子树为空,那就直接将那个子树接到原树上,然后就把原树挤掉了 
            k=t[k].l+t[k].r;
            else
            {
                if(t[t[k].l].rd<t[t[k].r].rd)//为了维持平衡树堆的性质,每次下沉都与随机权值小的交换 
                right(k),del(k,x);
                else
                left(k),del(k,x);
            }
        }
        else//如果还没找到要删除的数,那就继续找呗 
        {
            if(x>t[k].key)
            {
                t[k].sz--;
                del(t[k].r,x);
            }
            else
            {
                t[k].sz--;
                del(t[k].l,x);
            }
        }
    }
    int rank1(int k,int x)//查找数x的排名
    {
        if(k==0)return 0;
        if(t[k].key==x)return t[t[k].l].sz+1;//找到目标数,加上自己与比自己小的(即左子树)的数的个数 
        else
        if(x>t[k].key)
        return t[t[k].l].sz+t[k].re+rank1(t[k].r,x);//一旦在右子树寻找就要,递归回来时就要加上左子树大小 
        else
        return rank1(t[k].l,x);//如果在左子树找的话就不用加了 
    }
    int rank2(int k,int x)//查找排名为x的数
    {
        if(k==0)return 0;
        if(x<=t[t[k].l].sz)//在左子树中找 
        return rank2(t[k].l,x);
        else
        if(x>(t[t[k].l].sz+t[k].re))
        return rank2(t[k].r,x-t[t[k].l].sz-t[k].re);//在右子树中找 
        else
        return t[k].key;//如果既不在左子树,也不在右子树,那就在这个结点上了,就是这个结点的数 
    }
    void pre(int k,int x)//找前缀 
    {
        if(k==0)return;
        if(t[k].key<x)
        {
            ans=k;//每次都更新ans的值,直到找到最值 
            pre(t[k].r,x);
        }
        else
        pre(t[k].l,x);//显然没找到符合要求的,那就继续找 
    }
    void next(int k,int x)//找后缀 
    {
        if(k==0)return;
        if(t[k].key>x)
        {
            ans=k;//与找前缀同理 
            next(t[k].l,x);
        }
        else
        next(t[k].r,x);
    }
    int main()
    {
        srand(time(0));//好像这行代码可加可不加,本来就是只为了使随机数每次不同,至于为何删掉后没影响,我就不知道了 
        int n;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
        {
            int op,x;
            scanf("%d%d",&op,&x);
            if(op==1)
            inin(root,x);
            if(op==2)
            del(root,x);
            if(op==3)
            {
                int res=rank1(root,x);
                printf("%d
    ",res);
            }
            if(op==4)
            {
                int res=rank2(root,x);
                printf("%d
    ",res);
            }
            if(op==5)
            {
                pre(root,x);
                printf("%d
    ",t[ans].key);
            }
            if(op==6)
            {
                next(root,x);
                printf("%d
    ",t[ans].key);
            }
        }
        return 0;
    }
  • 相关阅读:
    关于sencha touch中给文本添加焦点无效的解决方案
    sencha touch 入门系列 (五)sencha touch运行及代码解析(上)
    关于用phonegap 3.0+ 打包后sencha touch按钮点击切换动画延迟接近一秒的以及界面闪烁的解决方案
    Building a Simple User Interface(创建一个简单的用户界面)
    Running Your App(运行你的应用程序)
    android GridLayout布局
    Android Studio SVN的使用
    Android Library项目发布到JCenter最简单的配置方法
    AndroidStudio项目提交(更新)到github最详细步骤
    Android RecyclerView的使用
  • 原文地址:https://www.cnblogs.com/genius777/p/8470579.html
Copyright © 2011-2022 走看看