首页 > 其他分享 > P3369普通平衡树 学习笔记

P3369普通平衡树 学习笔记

时间:2022-12-24 17:34:34浏览次数:45  
标签:左子 结点 BST ll 右子 笔记 son P3369 平衡

题意

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

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

思路

BST

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

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

BST的性质:

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

Treap

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

旋转操作

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

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

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

插入操作

  • 如果当前没有结点,那么就到了位置,直接创建新的。
  • 如果当前结点的键值为 \(x\),那么懒添加,在当前结点的出现次数记录那里直接 \(+1\) 即可。
  • 否则根据BST性质接着遍历即可。
  • 插入后如果不满足Trep的堆性质了,旋转。

删除操作

  • 如果不存在,那么返回即可。
  • 如果要删除的元素是在子树中,去子树里找
  • 如果当前要删除结点是叶子,那么直接删除。
  • 如果当前点只有一个孩子,那么直接把自己跟孩子换了就行。
  • 否则有两个孩子的话旋转一下后直接删除。

查询x数的排名

  • 如果当前节点是x,直接返回左子树大小 \(+1\) 即可。
  • 否则接着安照BST性质遍历即可。

查询排名为x的数

  • 与上一个操作很像,读者可以自行想想

前驱后继

读者也可以自行想想。

tips

我们为什么不直接用BST呢?可以想想,如果给出一个单调递增/递减的序列,是不是都一直往右子树/左子树上添加了。一个满二叉树(最好情况)的时间复杂度(层数)是 \(\log{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;
}

标签:左子,结点,BST,ll,右子,笔记,son,P3369,平衡
From: https://www.cnblogs.com/tanghg/p/17003047.html

相关文章

  • Spring IOC官方文档学习笔记(四)之依赖项(下)
    3.depends-on(1)depends-on用来表示一个bean的实例化依靠另一个bean的先实例化,如果在一个beanA上定义了depends-onbeanB就表示:beanA实例化前先实例化beanB。<!--......
  • 绿盟 2020年数据安全前沿技术研究报告 学习笔记2和下载地址
    多方​​数据安全​​的联合AI建模参考资料​​绿盟2020数据安全前沿技术研究报告​​数据驱动AI建模,一般来说,模型效果与训练数据的特征维度与样本规模密切相关。然而,在......
  • 联通 DevSecOps 实践白皮书 学习笔记和下载地址
    ​​联通DevSecOps实践白皮书2021​​概述数字化技术催生各行业的不断创新:ICT、媒体、金融、保险在数字化发展曲线中已经独占鳌头,零售、汽车、油气化工、健康、矿业、农......
  • 27款笔记软件的介绍
    [长文警告,鼠标移至文章标题,右边会显示按钮,点击可显示文章目录导航]初衷工欲善其事必先利其器。一开始是想找一款合适自己的笔记软件,因为随着记录的东西的增多,越来越觉得......
  • Docker学习笔记
    一、容器化技术相关概念1、容器化技术概念在软件开发过程中环境配置永远是最让人头疼的在开发之前我们需要准备各种运行环境、IDE及辅助工具同时软件部署也为程序员的谢......
  • Python学习笔记--从继承开始继续
    继承的基础语法单继承:多继承:一个子类继承多个父类pass关键字补全语法注意事项:复写和使用父类成员复写父类成员也就是相当于Java中的方法重写调用父类成员......
  • Python学习笔记--第二阶段啦
    初识对象示例:类的成员方法上图中的self必须填写!!!示例:类和对象有c和c++语言基础的话,就会发现其实是一样的道理,只是实现代码有差异构造方法(init)示例:注意:......
  • FreeSWITCH学习笔记:EventSocket
    本文更新于2022-12-20,使用FreeSWITCH1.10.7。目录apiauthbgapiconnectdivert_eventseventexitfilterfilterdeletelingerlogmyeventsnixeventnoeventnolingernologsendev......
  • 边分治 学习笔记
    边分治学习笔记就普遍理性而论,边分治能做的点分治也能做,可是难度…参考博客:边分治讲解前置:多叉树转二叉树也叫三度化。边分治在二叉树上表现得很优秀,是\(O(nlogn)\)......
  • 废物利用,笔记本显示器拆机使用
    我自己有个老笔记本已经坏了,本来打算扔掉,忽然有个想法,可以吧显示器拆下来使用啊。于是上网查资料,还真可以。特此记录,方便以后查找。下面是步骤。把笔记本显示器拆下来。......