平衡树讲解(旋转treap,非旋转treap,splay)

在刷了许多道平衡树的题之后,对平衡树有了较为深入的理解,在这里和大家分享一下,希望对大家学习平衡树能有帮助。

平衡树有好多种,比如treap,splay,红黑树,STL中的set。在这里只介绍几种常用的:treap和splay(其中treap包括旋转treap和非旋转treap)。

一、treap

treap这个词是由tree和heap组合而成,意思是树上的的堆(其实就是字面意思啦qwq)。treap可以说是由二叉搜索树(BST)进化而来,二叉搜索树每个点满足它左子树中所有点权值都比它小,它右子树中所有点权值都比它大,这样二叉搜索树的中序遍历出来的序列权值就是从小到大有顺序的。对于一棵完全二叉搜索树,查询每个点的时间复杂度是O(logn)。但二叉搜索树很容易就会退化成一条链(顺序或逆序插入所有点),这样它就失去了原有的作用,于是便有了treap,treap就是在维护BST性质的同时还要维护小根堆(其实大根堆也可以)的性质——每个点的另一个权值比它所有子树上节点的都小,那么这个权值是什么呢?自然是随机数了!只有随机数才能使它成为一棵平衡树(层数在logn层左右)。那么怎么同时维护这两种数据结构的性质呢?由此就产生了旋转treap和非旋转treap(具体原理下面再讲)。

treap作为一种平衡树,既可以维护集合,也可以维护序列(splay也同样)。这两者有什么区别呢?维护集合的treap的每个点的权值(具体地说是维护BST性质的权值)是集合中每个数的具体数值,但维护序列的treap的每个点的权值是序列中每个数的下标(也就是这个数在序列中的位置),而这个数具体是什么不影响平衡树的结构,只是在求解时需要的一个数值。一般维护序列的题刚开始都会先给你一个序列,而维护集合的题每个数都是在过程中插入平衡树中的。

1、旋转treap

旋转treap维护BST和堆的性质是靠旋转实现的,旋转只有两种:左旋和右旋。如图所示。

因为在插入或删除一个数时可能会在树中(而不是在叶子节点)添加或减掉一个点,所以一定会改变树的结构,也就有可能使treap的性质不满足,这时就要用旋转操作来再次恢复treap的性质。旋转treap在维护集合插入时可以把相同权值的的数放在同一个点,也可以建立不同的点来存,如何存要因题而异。

介绍旋转treap的几种常见操作(以相同权值放在同一个点为例):

变量声明:size[x],以x为根节点的子树大小;ls[x],x的左儿子;rs[x],x的右子树;r[x],x节点的随机数;v[x],x节点的权值;w[x],x节点所对应的权值的数的个数。

1)左旋和右旋

以上图为例,左旋即把Q旋到P的父节点,右旋即把P旋到Q的父节点。

以右旋为例:因为Q>B>P所以在旋转之后还要满足平衡树性质所以B要变成Q的左子树。在整个右旋过程中只改变了B的父节点,P的右节点和父节点,Q的左节点的父节点,与A,B,C的子树无关。

void rturn(int &x)
{
    int t;
    t=ls[x];
    ls[x]=rs[t];
    rs[t]=x;
    size[t]=size[x];
    up(x);
    x=t;
}
void lturn(int &x)
{
    int t;
    t=rs[x];
    rs[x]=ls[t];
    ls[t]=x;
    size[t]=size[x];
    up(x);
    x=t;
}

2)查询

我们以查询权值为x的点为例,从根节点开始走,判断x与根节点权值大小,如果x大就向右下查询,比较x和根右儿子大小;如果x小就向左下查询,直到查询到等于x的节点或查询到树的最底层。

3)插入

插入操作就是遵循平衡树性质插入到树中。对于要插入的点x和当前查找到的点p,判断x与p的大小关系。注意在每次向下查找时因为要保证堆的性质,所以要进行左旋或右旋。

void insert_sum(int x,int &i)
{
    if(!i)
    {
        i=++tot;
        w[i]=size[i]=1;
        v[i]=x;
        r[i]=rand();
        return ;
    }
    size[i]++;
    if(x==v[i])
    {
        w[i]++;
    }
    else if(x>v[i])
    {
        insert_sum(x,rs[i]);
        if(r[rs[i]]<r[i])
        {
            lturn(i);
        }
    }
    else
    {
        insert_sum(x,ls[i]);
        if(r[ls[i]]<r[i])
        {
            rturn(i);
        }
    }

    return ;
}

4)上传

每次旋转后因为子树有变化所以要修改父节点的子树大小。

void up(int x)
{
    size[x]=size[rs[x]]+size[ls[x]]+w[x];
}

5)删除

删除节点的方法和堆类似,要把点旋到最下层再删,如果一个节点w不是1那就把w--就行。

void delete_sum(int x,int &i)
{
    if(i==0)
    {
        return ;
    }
    if(v[i]==x)
    {
        if(w[i]>1)
        {
            w[i]--;
            size[i]--;
            return ;
        }
        if((ls[i]*rs[i])==0)
        {
            i=ls[i]+rs[i];
        }
        else if(r[ls[i]]<r[rs[i]])
        {
            rturn(i);
            delete_sum(x,i);
        }
        else
        {
            lturn(i);
            delete_sum(x,i);
        }
        return ;
    }
    size[i]--;
    if(v[i]<x)
    {
        delete_sum(x,rs[i]);
    }
    else
    {
        delete_sum(x,ls[i]);
    }
    return ;
}

推荐练习题:

BZOJ3224普通平衡树

NOIP2017列队

BZOJ1208[HNOI2004]宠物收养场

BZOJ1503[NOI2004]郁闷的出纳员

BZOJ3196二逼平衡树

2、非旋转treap

非旋转treap相对于旋转treap更加简单暴力一些,只要断裂和合并两个操作就能维护树的平衡及所有操作(起码我所知的所有操作qwq),它相对于旋转treap能实现区间操作及可持久化且代码简短(对于我来说是不存在的QAQ)。

介绍一下这两个操作:

1)断裂

就是去掉一条边,把treap拆分成两棵树,对于区间操作可以进行两次断裂来分割出一段区间再进行操作。

以查找value为例,从root往下走,如果v[x]>value,那么下一步走ls[x],之后的点都比x小,把x接到右树上,下一次再接到右树上的点就是x的左儿子。

v[x]<=value与上述类似,在这里不加赘述。

void split(int x,int &lroot,int &rroot,int val)
{
    if(!x)
    {
        lroot=rroot=0;
        return ;
    }
    if(v[x]<=val)
    {
        lroot=x;
        split(rs[x],rs[lroot],rroot,val);
    }
    else
    {
        rroot=x;
        split(ls[x],lroot,ls[rroot],val);
    }
    up(x);
}

2)合并

就是把断裂开的树合并起来,因为要维护堆的性质所以按可并堆来合并。

void merge(int &x,int a,int b)
{
    if(!a||!b)
    {
        x=a+b;
        return ;
    }
    if(r[a]<r[b])
    {
        x=a;
        merge(rs[x],rs[a],b);
    }
    else
    {
        x=b;
        merge(ls[x],a,ls[b]);
    }
    up(x);
}

其他操作只要把treap断裂开,对对应区间或点进行操作再合并回去就OK了。

推荐练习题:

BZOJ3224普通平衡树

BZOJ3223文艺平衡树

BZOJ1500[NOI2005]维修数列

二、splay

splay的意思是延展树,同样满足二叉搜索树的性质,只不过splay维护平衡的方法只是旋转。每次查询会调整树的结构,使被查询频率高的条目更靠近树根。因此,就算刚开始时是一条链,在操作过程中也会变成正常的树。

splay一共有六种旋转方式,其中最基础的两种就是treap的那两种,其他四种都是由那两种演化来的。

基础的旋转只能向上转一层,因此有了向上转两层的操作。但转两层自然不会那么简单,旋转是要有顺序的,以上图将x旋到g位置为例,要先将p选上去,再将x旋上去,也就是从上往下旋。

                  

而像这种情况中将x旋到g位置,要先将x旋到p处,再旋到g处,也就是从下往上旋。

splay同样可以实现区间操作且在LCT中会用到,但splay不能可持久化。对于单点操作只需把这个点旋到根节点再查询有关信息即可,对于区间[x,y]操作,先将x-1旋到根节点,再将y+1旋到根节点的右儿子处,这样根节点右儿子的左儿子就是想要的区间。那么如何旋到根节点呢?只要两层两层往上旋就好了。

最后附上splay区间操作代码(以文艺平衡树区间翻转为例)

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
int n,m;
int root;
int son[100007][3];
int size[100007];
int val[100007];
int f[100007];
int tag[100007];
int key[100007];
int sum[100007];
int d[100007];
int x,y;
int total;
int INF=1e9;
int flag=0;
bool get(int x)
{
    return son[f[x]][1]==x;
}
void pushup(int x)
{
    size[x]=size[son[x][0]]+size[son[x][1]]+1;
}
void pushdown(int x)
{
    if(x&&tag[x])
    {
        tag[son[x][0]]^=1;
        tag[son[x][1]]^=1;
        swap(son[x][0],son[x][1]);
        tag[x]=0;
    }
}
void rotate(int x)
{
    int fa=f[x];
    int anc=f[fa];
    int k=get(x);
    pushdown(fa);
    pushdown(x);
    son[fa][k]=son[x][k^1];
    f[son[fa][k]]=fa;
    son[x][k^1]=fa;
    f[fa]=x;
    f[x]=anc;
    if(anc)
    {
        son[anc][son[anc][1]==fa]=x;
    }
    pushup(fa);
    pushup(x);
}
void splay(int x,int goal)
{
    for(int fa;(fa=f[x])!=goal;rotate(x))
    {
        if(f[fa]!=goal)
        {
            rotate((get(fa)==get(x))?fa:x);
        }
    }
    if(!goal)
    {
        root=x;
    }
}
int build(int fa,int l,int r)
{
    if(l>r)
    {
        return 0;
    }
    int mid=(l+r)>>1;
    int now=++total;
    key[now]=d[mid];
    f[now]=fa;
    tag[now]=0;
    son[now][0]=build(now,l,mid-1);
    son[now][1]=build(now,mid+1,r);
    pushup(now);
    return now;
}
int rank(int x)
{
    int now=root;
    while(1)
    {
        pushdown(now);
        if(x<=size[son[now][0]])
        {
            now=son[now][0];
        }
        else
        {
            x-=size[son[now][0]]+1;
            if(!x)
            {
                return now;
            }
            now=son[now][1];
        }
    }
}
void turn(int l,int r)
{
    l=rank(l);
    r=rank(r+2);
    splay(l,0);
    splay(r,l);
    pushdown(root);
    tag[son[son[root][1]][0]]^=1;
}
void write(int now)
{
    pushdown(now);
    if(son[now][0])
    {
        write(son[now][0]);
    }
    if(key[now]!=-INF&&key[now]!=INF)
    {
        if(flag==0)
        {
            printf("%d",key[now]);
            flag=1;
        }
        else
        {
            printf(" %d",key[now]);
        }
    }
    if(key[son[now][1]])
    {
        write(son[now][1]);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        d[i+1]=i;
    }
    d[1]=-INF;
    d[n+2]=INF;
    root=build(0,1,n+2);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        turn(x,y);
    }
    write(root);
    return 0;
}

原文地址:https://www.cnblogs.com/Khada-Jhin/p/9215468.html

时间: 2024-07-29 00:46:27

平衡树讲解(旋转treap,非旋转treap,splay)的相关文章

沉迷数据结构1(treap&amp;非旋treap)

Achen大佬说不要沉迷数据结构否则智商会降低的. 从省选考完后就开始学treap,首先是自己yy了一个打了两百多行,然后debug了2个月还是3个月记不清了. 最后弃疗,去找了网上别人的代码抄了一遍. noip考完后补常规的一段时间,羡慕Achen能20分钟打出一个treap模板,于是自己也开始走上打板子的不归路. 到了后来可以10分钟左右打出一个结构体版的treap,看了Achen的数组版treap,觉得自己结构体版的太不优秀啦,于是就换成数组版的. 然后现在有几周没有碰过treap,感觉又

[bzoj1895][Pku3580]supermemo_非旋转Treap

supermemo bzoj-1895 Pku-3580 题目大意:给定一个n个数的序列,需支持:区间加,区间翻转,区间平移,单点插入,单点删除,查询区间最小值. 注释:$1\le n\le 6.1\cdot 10^6$. 想法: 这数据范围给的我真是醉了. 显然用平衡树,这里用非旋转Treap,题目让你干什么你就干什么. 区间加:撕出子树区间后打标记维护区间加. 区间翻转:撕出子树区间后打标记维护区间翻转. 区间平移:相当于两段相邻区间交换,所以撕成四部分:左边,第一个区间,第二个区间,右边.

普通平衡树——非旋转treap

题目: 此为平衡树系列第一道:普通平衡树您需要写一种数据结构,来维护一些数,其中需要提供以下操作:1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个)3. 查询x数的排名(若有多个相同的数,因输出最小的排名)4. 查询排名为x的数5. 求x的前驱(前驱定义为小于x,且最大的数)6. 求x的后继(后继定义为大于x,且最小的数) n<=100000 所有数字均在-107到107内. 输入样例: 10 1 106465 4 1 1 317721 1 460929 1 644985 1 841

4923: [Lydsy1706月赛]K小值查询 平衡树 非旋转Treap

国际惯例的题面:这种维护排序序列,严格大于的进行操作的题都很套路......我们按照[0,k],(k,2k],(2k,inf)分类讨论一下就好.显然第一个区间的不会变化,第二个区间的会被平移进第一个区间,第三个区间的相对大小不会变化.于是我们直接把第二个区间拆了重构,一个一个插入第一个区间即可.因为每次这样做最少减半,所以每个元素只会被重构log次,复杂度nlog^2n.这种按照值域分离区间的操作,非旋转treap实现起来是最简单的......然而第一次写非旋转treap还是出了一点问题,注意它

非旋转Treap详解

利用其他人其中考试的时间,终于学完了非旋转Treap,它与普通Treap的区别就是它不旋转废话.前置知识只有BST和可并堆. BST看这个博客,解释的挺清楚的.https://www.cnblogs.com/jiangminghong/p/9999884.html 可并堆就是用很快的时间合并两个堆.如果裸上一个并查集的话就是nlog2n.这个复杂度我们是不能接受的.正常的可并堆是维护一棵左偏树,我们用一个参数dis[x]表示从x点出发能够向右走的最大步数.每次两个堆合并时,我们就把一个堆扔到另一

非旋转Treap

Treap是一种平衡二叉树,同时也是一个堆.它既具有二叉查找树的性质,也具有堆的性质.在对数据的查找.插入.删除.求第k大等操作上具有期望O(log2n)的复杂度.     Treap可以通过节点的旋转来实现其维持平衡的操作,详见旋转式Treap. 而旋转式Treap在对区间数据的操作上无能为力,这就需要非旋转式Treap来解决这些区间问题. 非旋转式Treap支持的操作 基本操作: 操作 说明 实现复杂度 Build 构造Treap O(n) Merge 合并Treap O(log2n) Sp

[bzoj3173]最长上升子序列_非旋转Treap

最长上升子序列 bzoj-3173 题目大意:有1-n,n个数,第i次操作是将i加入到原有序列中制定的位置,后查询当前序列中最长上升子序列长度. 注释:1<=n<=10,000,开始序列为空. 想法:显然,我们发现,我每次加入的数一定是当前序列中最大的,所以,刚刚加入的i,要么是当前序列中LIS的结尾,要么不属于LIS.根据这个性质,我们想到:在Treap中维护这样的性质,就是维护每个数加入节点的编号.然后,我们更新新节点的方式就是它的左子树和右子树的LIS取最大+1.其实最重要的就是如何加入

非旋Treap总结 : 快过Splay 好用过传统Treap

非旋$Treap$ 其高级名字叫$Fhq\ Treap$,既然叫$Treap$,它一定满足了$Treap$的性质(虽然可能来看这篇的人一定知道$Treap$,但我还是多说几句:$Fhp\ Treap$就是继承了$Treap$的随机系统,在二叉搜索的基础上,每个点加一个随机化处理,这些随机值满足堆的性质……通俗一点讲,就是$Fhp\ Treap$它每个点有至少两个值,一个是val,即存的数值,这些数值满足二叉搜索树,也就是父亲比左孩子小/大,则右孩子比父亲小/大:还有一个是key,是个随机值,这些

可持久化Treap(fhq Treap,非旋转式Treap)学习(未完待续)

简介: Treap,一种表现优异的BST 优势: 其较于AVL.红黑树实现简单,浅显易懂 较于Splay常数小,通常用于树套BST表现远远优于Splay 或许有人想说SBT,SBT我没有实现过,据说比较快 但是SBT.Splay以及旋转版Treap等BST都不可以比较方便地实现‘可持久化操作 Treap=Tree+Heap Treap是一颗同时拥有二叉搜索树和堆性质的一颗二叉树 Treap有两个关键字,在这里定义为: 1.key,满足二叉搜索树性质,即中序遍历按照key值有序 2.fix,满足堆