洛谷 3369 【模板】普通平衡树 treap

题目

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

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

题解

啊,平衡树!
表示蒟蒻的我只会(刚学)treap,于是就模板了一下最基础的那种treap(通过旋转操作维持平衡)

treap是什么呢,emmmm,就是一棵二叉查找树,然后每个点有一个随机出来的key值,对树上的点用key值维护一个大根(小根)堆。这样的一棵树,期望高度是log(n),所以期望的复杂度也是log(n)。
有6种操作
1.insert插入一个数
2.dele删除一个数(若有多个相同的数,因只删除一个)
3.rank查询 xx 数的排名(排名定义为比当前数小的数的个数+1 。若有多个相同的数,因输出最小的排名)
4.val查询排名为 xx 的数
5.pre求 xx 的前驱(前驱定义为小于 xx ,且最大的数)
6.next求 xx 的后继(后继定义为大于 xx ,且最小的数)
这些操作对于我来说是如此精妙,以至于代码里打满注释来让自己理解记忆

对于旋转操作,有
左旋 根结点变左儿子,右儿子变根节点,右儿子的左儿子变左儿子的右儿子
右旋 根结点变右儿子,左儿子变根节点,左儿子的右儿子变右儿子的左儿子

代码

#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstdlib>//随机数的头文件

using namespace std;

struct node{
    int l,r;
    int key,data,size;
}t[100005];
int n,root,cnt;
const int m=100000000;

void updata(int x){
    t[x].size=t[t[x].r].size+t[t[x].l].size+1;
}//更新size值,在插入、删除、换位置时要用

void rttn(int &x)
{
    int y=t[x].l;
    t[x].l=t[y].r;
    t[y].r=x;
//根结点变右儿子,左儿子变根节点,左儿子的右儿子变右儿子的左儿子
    updata(x);updata(y);//换位置后更新size值
    x=y;//x值会被赋值给记录整棵树根节点的root,而y是右旋后的根节点
}//传说中的右旋

void lttn(int &x)
{
    int y=t[x].r;
    t[x].r=t[y].l;
    t[y].l=x;
    updata(x);updata(y);
    x=y;
}//同上

void insert(int &x,int k){//插入
    if (x==0){
        x=++cnt;
        t[cnt].key=rand();
        //key值为随机数,用堆维护后可防止树退化
        t[cnt].data=k;
        t[cnt].size=1;
        return;
    }
//一直找到可以插入k值的叶子节点,此时x=0,执行插入操作
    if (k<=t[x].data){
        insert(t[x].l,k);//往左子树找
        if (t[x].key>t[t[x].l].key) rttn(x);
    } else {
        insert(t[x].r,k);//往右子树找
        if (t[x].key>t[t[x].r].key) lttn(x);
    }

    updata(x);//记得插入后要更新size值
}

void dele(int &x,int k){//删除
具体步骤:先找到要删除的数
如果这个数在叶子节点上就直接删除
删除就是把它的父亲连向该节点的t[x].l或t[x].r赋值为0
如果只有左儿子,就右旋,把要删除的数移到右儿子,递归删除右儿子
如果只有右儿子操作同上
如果左右儿子都有,就根据key值决定左旋或右旋,决定的依据
是保持大根(小根)堆性质,即保持key值大(小)的为根节点
    if (t[x].data==k){//找到要删除的数了
        if (t[x].l || t[x].r){
            if (t[x].l&&((!t[x].r) || (t[t[x].l].key<t[t[x].r].key))){
                rttn(x);
                dele(t[x].r,k);
                updata(x);//删除后要更新size值
            } else {
                lttn(x);
                dele(t[x].l,k);
                updata(x);//删除后要更新size值
            }
        } else x=0;
        return;
    }
    if (k<t[x].data) dele(t[x].l,k); else dele(t[x].r,k);
    //寻找要删除的数
    updata(x);//删除后要更新size值
}

int rank(int x,int k){
    if (x==0) return 0; 
    if (t[x].data>=k) return rank(t[x].l,k);
     else return rank(t[x].r,k)+t[t[x].l].size+1;                    
}
如果x大于当前子树的根,那么说明左儿子及树根都比x小,答案
加上Lson.size+1,并且往右儿子走;否则往左儿子走,不统计
答案。

int val(int x,int k){
    if (t[t[x].l].size+1==k) return t[x].data;
    if (k<t[t[x].l].size+1) return val(t[x].l,k); else
                            return val(t[x].r,k-t[t[x].l].size-1);
}
我们给每个节点记录一个size,表示以该节点为根的子树大小。
根据Treap性质,一个点在它的子树内的rank其实就是左子树的
大小+1,也就是说,如果K=Leftson.size+1,当前点即为答案,
如果K<Leftson.size+1,则往左儿子搜索第K大,否则往右儿子
搜索第K-(Lson.size+1)大。

int pre(int k){//前驱,比x小的数里最大的一个
    int ans=-m,x=root;
    while (x){
        if (k>t[x].data) ans=max(t[x].data,ans),x=t[x].r; 
        else x=t[x].l;
//虽然树中各点的大小是按顺序的,但是ans还是比较一下再赋值比较稳妥
    }
    return ans;
}
如果x大于当前子树的根,说明当前节点的值有可能作为答案,
去更新ans,然后往右儿子走;不然当前值不能更新答案,
往左儿子走。

int next(int k){//后继,同上,类似于前驱
    int ans=m,x=root;
    while (x){
        if (k<t[x].data) ans=min(ans,t[x].data),x=t[x].l; 
        else x=t[x].r;
    }
    return ans;
}

int main(){
    scanf("%d",&n);
    for (int i=1;i<=n;i++){
        int p,x;
        scanf("%d%d",&p,&x);
        if (p==1) insert(root,x);
        if (p==2) dele(root,x);
        if (p==3) printf("%d\n",rank(root,x)+1);        
        if (p==4) printf("%d\n",val(root,x));
        if (p==5) printf("%d\n",pre(x));
        if (p==6) printf("%d\n",next(x));
    }
}

猜你喜欢

转载自blog.csdn.net/yjy_aii/article/details/80882142