【学习笔记/模板】有旋Treap

update at 2022.6.16 修改了些晦涩难懂的地方。

调了一周,今天总算调出来了。

概述:

Treap=Tree+Heap,其既有二叉查找树 BST 的性质,又有堆 Heap 的性质,于是有能维护排名,有能保证深度在 Θ(logN) 的量级。

BST:

二叉查找树,满足保证根左侧子树的所有节点比根小,右侧的所有节点比根大的树(没有相同节点)。

操作:

  • 查询 x 的排名

    1. 只要将x与根比较,如果相等,排名为左子树元素个数 +1
    2. 如果比根小,递归查询他在左子树的排名,排名为他在左子树的排名空树排名为0
    3. 如果比根大,递归查询他在右子树的排名,排名为右子树的排名 + 左子树元素个数 +1;
  • 插入 x

    不断地判断x与根的大小关系,
    比根小,则递归左子树;
    比根大,则递归右子树,直到来到一个空树,插入。

  • 删除 x

    1. 如果一个节点是叶子节点直接删除
    2. 否则,如果这个节点有一个子节点,直接将其连接到该节点的父亲
    3. 否则,沿着右子树的根一路向左到底,然后用那个值替换掉要删除的节点。

总结:

BST 支持 Treap 的所有一般操作,功能齐全,实现简单,在随机数据下也比 Treap 等平衡树快很多。

BST 毕竟不能维护树的平衡, BST 的复杂度取决于它的平均深度,在特定数据下树会退化为链,使深度为线性,于是单次操作的复杂度会提升到 Θ(N) ,明显不够优。

于是,我们需要引入 Treap 的下一个性质: Heap

Heap:

即堆,是一种保证任意节点的左右儿子都比自身小的完全二叉树,其深度始终保持在 logN 的数量级,刚好符合了我们的需求。

操作:

  • 查询

    堆顶即为最值,直接调用即可。

  • 插入

    1. 将新节点插入二叉树底端,

    2. 然后让他不断上跳,直到它小于它的父亲或者自己为根。

  • 删除
    用二叉树底端的节点覆盖根,然后让新的根与左右儿子比较,用较大的儿子替换根,如此往复即可。

Treap:

Treap 就是集 BSTHeap 二者的性质于一身,即能够支持 BST 的操作,有能够保证 Heap 的深度。

可惜的是, BSTHeap 的性质似乎有些矛盾,前者是左子树 << 右子树,后者是根 < 左儿子 < 右儿子。

其实 Treap 的本质还是 BST ,对于任意节点,保证根左侧子树的所有节点比根小,右侧的所有节点比根大的树(没有相同节点)。
我们只是利用堆的性质,赋予每一个节点一个随机值,按照随机值维护堆的形状。
于是我们需要一个操作,既能保持 BST 的性质,又能够将根节点与儿子替换,于是我们需要 Treap 的核心——旋转操作

旋转:

rotate ,即旋转操作,分为 zig左旋和 zag右旋,其思想是一致的,也可以统一实现,故一起介绍。
rotate 的目标是将一个儿子移到根处,并且在此过程中保持BST的性质。

实现:

struct Tree{
int rd; //i节点的一个随机值,是在堆中的关键字
int val; //i节点的关键字
int num; //由于可能有重复,所以存储的是i节点关键字的个数
int size; //以i为根的子树的节点数
int ch[2]; //存储i节点的儿子,ch[i][0]表示左儿子,ch[i][1]表示右儿子
Tree(){
rd = val = num = size = 0;
ch[0] = ch[1] = 0;
}
}tr[MAXN];

为了方便,展示下宏定义

#define lson(x) tr[x].ch[0]
#define rson(x) tr[x].ch[1]
  • pushup 归并
    顾名思义,拿儿子更新父亲 rt 的节点数。
    rt 的节点数 = 左右儿子节点数之和 + rt 本身存有数量
void Pushup(int rt){
tr[rt].size = tr[lson(rt)].size + tr[rson(rt)].size + tr[rt].num;
}
  • rotate旋转
    rotate(&rt ,d) ——
    rt 为根(可能有变)旋转,d=0右旋, d=1 左旋。
void Rotate(int &rt, int d){ //d = 0时是右儿子旋上来,即左旋, d = 1是左儿子旋上来,即右旋
int son = tr[rt].ch[d ^ 1];
tr[rt].ch[d ^ 1] = tr[son].ch[d];
tr[son].ch[d] = rt;
Pushup(rt);
Pushup(son);
rt = son;
} //之前这玩意简直不是人写出来的
  • insert插入
    因为 treap 在其他操作过程中是并不改变树的形态的,所以在插入或是删除时要先找到要插入/删除的节点的位置,然后再创建一个新的节点/删除这个节点。

    然后考虑到插入/删除后树的形态有可能会改变,所以要考虑要通过旋转维护 treap 的形态。

    可以直接按照中序遍历结果找到最终的对应位置,然后再通过随机值维护它堆的性质。

void Insert(int &rt, int val){
if(!rt){ //找到对应位置就新建节点
rt = ++tot;
tr[rt].size = tr[rt].num = 1;
tr[rt].val = val;
tr[rt].rd = rand();
return;
}
tr[rt].size++; //因为插入了数,所以在路径上每个节点的size都会加1
if(tr[rt].val == val){
tr[rt].num++;
return;
} //找到了直接返回
int d = val > tr[rt].val ? 1 : 0;
Insert(tr[rt].ch[d], val); //否则递归查找插入位置
if(tr[rt].rd > tr[tr[rt].ch[d]].rd)
Rotate(rt, d ^ 1); //小修一波
Pushup(rt);
}
  • delete 删除:
    1. 先找到要删除的节点的位置。
    2. 如果这个节点位置上有多个相同的数,则直接 num
    3. 如果只有一个儿子或者没有儿子,直接将那个儿子接到这个节点下面(或将儿子赋值为0)。
    4. 如果有两个儿子,现将随机值小的那个旋到这个位置,将根旋下去,然后将旋之后的情况转化为前几种情况递归判断。
void Delete(int &rt, int val){
if(!rt) return;
if(val < tr[rt].val)
Delete(lson(rt), val);
else if(val > tr[rt].val)
Delete(rson(rt), val);
else{
if(!lson(rt) && !rson(rt)){
tr[rt].num--;
tr[rt].size--;
if(!tr[rt].num) rt = 0;
}
else if(lson(rt) && !rson(rt)){
Rotate(rt, 1);
Delete(rson(rt), val);
}
else if(!lson(rt) && rson(rt)){
Rotate(rt, 0);
Delete(lson(rt), val);
}
else if(lson(rt) && rson(rt)){
int d = tr[lson(rt)].rd > tr[rson(rt)].rd ? 1 : 0;
Rotate(rt, d);
Delete(tr[rt].ch[d], val);
}
}
Pushup(rt);
} //大修大改
  • rank 查寻排名
    用递归的方式求解,用到的目前这个点的权值作为判断的依据,并在找到节点的路上不断累加小于该权值的个数。
int Rank(int rt, int val){
if(!rt) return 0;
if(tr[rt].val == val)
return tr[lson(rt)].size + 1; //找到了就返回最小的那个
if(tr[rt].val > val)
return Rank(lson(rt), val); //如果查找的数在x的左边,则直接往左边查
else
return Rank(rson(rt), val) + tr[lson(rt)].size + tr[rt].num; //否则往右边查,左边的所有数累加进答案
}
  • find 查询第 k 小的数
    只需要找到中序遍历中的第k个,也是递归求解。
int Find(int rt, int pos){
if(!rt) return 0;
if(tr[lson(rt)].size >= pos)
return Find(lson(rt), pos);
else if(tr[lson(rt)].size + tr[rt].num < pos)
return Find(rson(rt), pos - tr[rt].num - tr[lson(rt)].size);
else
return tr[rt].val;
}
  • pre/suf 查找前驱/后继
    仍然是因为不能改变树的形态,需要递归求解。
    同样的,找一个节点的前驱,就直接在它左半边中找一个最大值就可以了。
    如果是在这个节点的右边的话,就一直向下递归。
    如果递归有一个分支直到叶子节点以下都一直没找到一个比该权值要小的值,那么最后要返回一个-inf/inf来防止答案错误(同时找到叶子节点下面也是要及时return防止越界)。
int Pre(int rt, int val){
if(!rt) return -INF; //防止越界,同时-INF无法更新答案
if(tr[rt].val >= val)
//如果该节点的权值大于等于要找的权值
//则不能成为前驱,递归查找左子树(有可能找到前驱)
return Pre(lson(rt), val);
else //找右子树中是否存在前驱
return max(Pre(rson(rt), val), tr[rt].val);
}
int Suf(int rt, int val){ //同上
if(!rt) return INF;
if(tr[rt].val <= val)
return Suf(rson(rt), val);
else
return min(Suf(lson(rt), val), tr[rt].val);
}
  • 这样的,一棵 Treap 就完工了

完整Code:

例题:P3369 【模板】普通平衡树

#include<cmath>
#include<ctime>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define lson(x) tr[x].ch[0]
#define rson(x) tr[x].ch[1]
using namespace std;
const int MAXN = 1e5 + 10;
const int INF = 1e9;
int n, a, root, ans;
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
struct Treap{
int tot;
struct Tree{
int rd;
int val;
int num;
int size;
int ch[2];
Tree(){
rd = val = num = size = 0;
ch[0] = ch[1] = 0;
}
}tr[MAXN];
Treap(){
tot = 0;
}
void Pushup(int rt){
tr[rt].size = tr[lson(rt)].size + tr[rson(rt)].size + tr[rt].num;
}
void Rotate(int &rt, int d){
int son = tr[rt].ch[d ^ 1];
tr[rt].ch[d ^ 1] = tr[son].ch[d];
tr[son].ch[d] = rt;
Pushup(rt);
Pushup(son);
rt = son;
} //之前这玩意简直不是人写的
void Insert(int &rt, int val){
if(!rt){
rt = ++tot;
tr[rt].size = tr[rt].num = 1;
tr[rt].val = val;
tr[rt].rd = rand();
return;
}
tr[rt].size++;
if(tr[rt].val == val){
tr[rt].num++;
return;
}
int d = val > tr[rt].val ? 1 : 0;
Insert(tr[rt].ch[d], val);
if(tr[rt].rd > tr[tr[rt].ch[d]].rd)
Rotate(rt, d ^ 1);
Pushup(rt);
}
void Delete(int &rt, int val){
if(!rt) return;
if(val < tr[rt].val)
Delete(lson(rt), val);
else if(val > tr[rt].val)
Delete(rson(rt), val);
else{
if(!lson(rt) && !rson(rt)){
tr[rt].num--;
tr[rt].size--;
if(!tr[rt].num) rt = 0;
}
else if(lson(rt) && !rson(rt)){
Rotate(rt, 1);
Delete(rson(rt), val);
}
else if(!lson(rt) && rson(rt)){
Rotate(rt, 0);
Delete(lson(rt), val);
}
else if(lson(rt) && rson(rt)){
int d = tr[lson(rt)].rd > tr[rson(rt)].rd ? 1 : 0;
Rotate(rt, d);
Delete(tr[rt].ch[d], val);
}
}
Pushup(rt);
}
int Rank(int rt, int val){
if(!rt) return 0;
if(tr[rt].val == val)
return tr[lson(rt)].size + 1;
if(tr[rt].val > val)
return Rank(lson(rt), val);
else
return Rank(rson(rt), val) + tr[lson(rt)].size + tr[rt].num;
}
int Find(int rt, int pos){
if(!rt) return 0;
if(tr[lson(rt)].size >= pos)
return Find(lson(rt), pos);
else if(tr[lson(rt)].size + tr[rt].num < pos)
return Find(rson(rt), pos - tr[rt].num - tr[lson(rt)].size);
else
return tr[rt].val;
}
int Pre(int rt, int val){
if(!rt) return -INF;
if(tr[rt].val >= val)
return Pre(lson(rt), val);
else
return max(Pre(rson(rt), val), tr[rt].val);
}
int Suf(int rt, int val){
if(!rt) return INF;
if(tr[rt].val <= val)
return Suf(rson(rt), val);
else
return min(Suf(lson(rt), val), tr[rt].val);
}
}T;
int main(){
srand(time(0));
n = read();
for(register int i = 1; i <= n; i++){
int opt, x;
opt = read(), x = read();
if(opt == 1) T.Insert(root, x);
else if(opt == 2) T.Delete(root, x);
else if(opt == 3) printf("%d\n", T.Rank(root, x));
else if(opt == 4) printf("%d\n", T.Find(root, x));
else if(opt == 5) printf("%d\n", T.Pre(root, x));
else if(opt == 6) printf("%d\n", T.Suf(root, x));
}
return 0;
}
posted @   TSTYFST  阅读(85)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示