P3369普通平衡树

题意

写一棵平衡树,需要实现如下几种操作:

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

思路

BST

什么是BST(二叉查找树)呢?

BST是一种二叉树,用于做一些查找操作(其实很多是题意中所说明的工作)。BST需要满足当前结点的左子树中所有结点的键值要小于当前结点,而右子树相反,所有结点的键值都要大于当前节点。

BST的性质:

  • 一棵BST能对应一个键的集合
  • 一个集合对应的BST不为一
  • 对BST做中序遍历,得到的序列是严格单调递增的

Treap

Treap是一种编程比较简单的平衡树,它在普通二叉搜索树的基础上,给每个结点一个随机的优先级。树上每个结点在满足BST的性质以外,还需要根据优先级满足堆的性质。

旋转操作

为了要让Treap同时满足BST和堆的性质,肯定要对其结构进行调整。这个调整的方式被称为旋转。在Treap中,只有两种旋转操作——左旋和右旋

左旋一个子树,会把它的根挪到根的左子树部分,同时根的右子树成为子树的根,再把右子树的左子树接到根的右子树上。

右旋一个子树,会把它的根转到右子树上,同时根的左子树成为子树的根。在把左子树的右子树接到根的右子树上。

插入操作
  • 如果当前没有结点,那么就到了位置,直接创建新的。
  • 如果当前结点的键值为 x x x,那么懒添加,在当前结点的出现次数记录那里直接 + 1 +1 +1 即可。
  • 否则根据BST性质接着遍历即可。
  • 插入后如果不满足Trep的堆性质了,旋转。
删除操作
  • 如果不存在,那么返回即可。
  • 如果要删除的元素是在子树中,去子树里找
  • 如果当前要删除结点是叶子,那么直接删除。
  • 如果当前点只有一个孩子,那么直接把自己跟孩子换了就行。
  • 否则有两个孩子的话旋转一下后直接删除。
查询x数的排名
  • 如果当前节点是x,直接返回左子树大小 + 1 +1 +1 即可。
  • 否则接着安照BST性质遍历即可。
查询排名为x的数
  • 与上一个操作很像,读者可以自行想想
前驱后继

读者也可以自行想想。

tips

我们为什么不直接用BST呢?可以想想,如果给出一个单调递增/递减的序列,是不是都一直往右子树/左子树上添加了。一个满二叉树(最好情况)的时间复杂度(层数)是 log ⁡ n \log{n} logn 的,但是如果都往一个方向,那么层数就变成 n n n 了,这不好。但是如果旋转,每一次都能减少一层,然后随机化也会让这个序列达到一个相对平衡的状态。所谓的堆性质也可以理解为是一个“莫须有”的罪名罢了。

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long ll;
const ll MAXN=1e5+5;
const ll INF=2147483647;
ll root;
struct Node{
    ll son[2];//0为左孩子
    ll cnt,value,pri,size;
    //出现次数,值,优先级,个数
}t[MAXN];
void push_up(ll u){
    t[u].size=t[t[u].son[0]].size+t[t[u].son[1]].size+t[u].cnt;
    //为左子树的大小+右子树的大小+自身的个数
}
void rotate(ll &u,ll d){
    //d=0为左旋,d=1为右旋
    ll k=t[u].son[d^1];
    t[u].son[d^1]=t[k].son[d];
    t[k].son[d]=u;
    push_up(u);
    push_up(k);
    u=k;
}
ll sz;
void insert(ll &u,ll x){
    if(u==0){
        u=++sz;//新元素
        t[u].cnt=t[u].size=1;//叶子大小和元素个数都为1
        t[u].value=x;//值为x
        t[u].pri=rand();//随机赋优先级
        return;
    }
    if(t[u].value==x) {
        t[u].size++;//更改空间
        t[u].cnt++;//多了一个一样的
        return;
    }
    ll k=x>t[u].value;//在左子树还是右子树里
    insert(t[u].son[k],x);
    if(t[u].pri<t[t[u].son[k]].pri){//是否满足堆性质
        rotate(u,k^1);//旋转,旋哪个子树
    }
    push_up(u);//更新
}
void del(ll &u,ll x){
    if(u==0){
        return;//压根不存在
    }
    if(x<t[u].value){
        del(t[u].son[0],x);//在左子树里
    }else if(x>t[u].value){
        del(t[u].son[1],x);//在右子树里
    }else{
        if(t[u].cnt>1){
            t[u].size--;//懒删除
            t[u].cnt--;//同上
        }else if(t[u].son[0]==0&&t[u].son[1]==0){
            u=0;//叶子结点,直接删除就行了
        }else if(t[u].son[0]!=0&&t[u].son[1]==0){
            u=t[u].son[0];//接到左子树上
        }else if(t[u].son[0]==0&&t[u].son[1]!=0){
            u=t[u].son[1];//接到右子树上
        }else{
            ll k=t[t[u].son[0]].pri>t[t[u].son[1]].pri;//两边都有,选择删
            rotate(u,k);//旋一下,把根换到底下
            del(t[u].son[k],x);//接着删
        }
    }
    push_up(u);//更新
}
ll x_rank(ll u,ll x){
    if(u==0){
        return 0;//不存在
    }
    if(t[u].value==x){
        return t[t[u].son[0]].size+1;//小的元素个数再+1
    }
    if(x<t[u].value){
        return x_rank(t[u].son[0],x);//比当前小,在左子树里
    }
    return t[t[u].son[0]].size+t[u].cnt+ x_rank(t[u].son[1],x);//左子树的大小+当前结点元素个数+右子树中的排名
}
ll rank_x(ll u,ll x){
    if(t[t[u].son[0]].size>=x){
        return rank_x(t[u].son[0],x);//左子树的排名比x大
    }
    if(t[t[u].son[0]].size+t[u].cnt<x){
        return rank_x(t[u].son[1],x-t[t[u].son[0]].size-t[u].cnt);//这个结点的排名比x小,去右边,同时减掉当前排名
    }
    return t[u].value;//找到了,返回元素
}
ll pre(ll u,ll x){
    if(u==0){
        return -INF;//不存在前驱
    }
    if(t[u].value>=x){
        return pre(t[u].son[0],x);//比x大,去左边
    }
    return max(t[u].value, pre(t[u].son[1],x));//是不是当前的,还是在右子树里,取max
}
ll suc(ll u,ll x){
    if(u==0){
        return INF;//不存在
    }
    if(t[u].value<=x){
        return suc(t[u].son[1],x);//比x小,去右边
    }
    return min(t[u].value, suc(t[u].son[0],x));//选最小的,与前驱相同
}
int main(){
    srand(time(NULL));
    ll n;
    scanf("%lld",&n);
    for (int i = 1; i <=n ; ++i) {
        ll op,x;
        scanf("%lld%lld",&op,&x);
        if(op==1){
            insert(root,x);
        }else if(op==2){
            del(root,x);
        }else if(op==3){
            printf("%lld\n",x_rank(root,x));
        }else if(op==4){
            printf("%lld\n",rank_x(root,x));
        }else if(op==5){
            printf("%lld\n",pre(root,x));
        }else{
            printf("%lld\n",suc(root,x));
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值