前置技能:平衡树前传:BST

终于学到我们喜闻乐见的平衡树啦!

所以我们这次讲的是平衡树中比较好写的\(Treap\).

(以后会写splay的先埋个坑在这)

好了,进入正题.

step 1

我们知道,BST虽然很方便,

但是,它很容易被卡成一条链.

因此,我们需要一个能够保持平衡的BST.

于是就有了我们众所周知的平衡树.

而平衡树保持平衡的方法,据本蒟蒻所知就是旋转节点.

通过旋转BST的节点,既保持BST的性质,又使它变得平衡.

而旋转其实也很好理解,

先看这张丑陋的图:

(其中\(x\),\(y\)为节点,\(A\),\(B\),\(C\)为子树)

然后我们假设\(A\),\(B\)中有很多点,而\(C\)中只有很少的点.

于是,我们要通过旋转来使平衡树变平衡.

我们可以知道,\(B\)中的点的权值都是大于\(x\)而小于\(y\)的(等于全看个人爱好qwq),

于是,我们可以这么一转:

这样,既保持了BST的性质(自己仔细想一下就能明白了),又变得更加平衡了.

而旋转的过程也很简单,

上面我们是将\(y\)的左儿子\(x\)转到它的位置,

根据图片,我们可以看到,

\(y\)就变成了\(x\)的右儿子,而\(x\)的右儿子就变成了它的左儿子.

所以这就非常简单了,看代码吧:

inline void l_rotate(int &p){//将p的左儿子转到p的位置
int q=t[p].l;
t[p].l=t[q].r;t[q].r=p;p=q;
update(t[p].r);update(p);//update根据题目来定
}

而将右儿子转上来就刚好相反:

inline void r_rotate(int &p){
int q=t[p].r;
t[p].r=t[q].l;t[q].l=p;p=q;
update(t[p].l);update(p);
}

另外,其实我们可以发现,

如果是将\(y\)的左儿子转上来,

那么\(y\)就变成了\(x\)的相反方向的儿子(即右儿子),

而\(x\)的右儿子就补上了\(y\)的左儿子.

所以,旋转可以直接合并成一个函数(这一点会在splay里面讲的,所以就先不具体说了).

那么,旋转讲完了,

然而到底怎样才能让树平衡,要什么时候旋转呢?

我们发现,在随机的数据下,普通的BST就是接近平衡的,

所以,Treap的平衡也就是这样——听天由命,

给每个节点另外给一个随机的权值\(dat\),

然后保证\(dat\)满足堆性质(这里我们以大根堆为例),

也就是说,如果一个节点的\(dat\)小于它儿子节点,它就该转了.

那么这样,我们就能实现平衡啦.(并且Treap其实也就是Tree和Heap的合成词哦)

接下来,就该讲Treap的应用了:

step 2

其实,Treap的几个应用和BST并没有多大区别,

所以在BST中讲过的几个操作就直接看代码吧(主要是看看旋转的操作):

inline void insert(int &p,int val){
if(!p){p=New(val);return ;}
if(t[p].val==val) t[p].cnt++;
else insert(val<t[p].val? t[p].l:t[p].r,val);
if(t[t[p].l].dat>t[p].dat) l_rotate(p);//检查一下是否满足堆性质再旋转
if(t[t[p].r].dat>t[p].dat) r_rotate(p);
update(p);
} //接下来的删除似乎改的比较多哈(一开始忘记了qwq)
//在这里,我们就不用寻找代替的节点了,
//直接把它转到叶子节点再删就行啦 inline void remove(int &p,int val){
if(!p) return ;
if(t[p].val==val){
if(t[p].cnt>1){t[p].cnt--;update(p);return ;}
if(!t[p].l&&!t[p].r){p=0;return ;}//叶子节点直接删
if(!t[p].r||t[t[p].l].dat>t[t[p].r].dat) l_rotate(p),remove(t[p].r,val);//如果没有右儿子或左儿子的dat更大就把左儿子转上来再删
else r_rotate(p),remove(t[p].l,val);
update(p);return ;
}
remove(val<t[p].val? t[p].l:t[p].r,val);update(p);
} //接下来的就和BST一样了
inline int getnext(int val){
int p=root,ans=2;
while(p){
if(t[p].val==val){
if(!t[p].r) break;
p=t[p].r;while(t[p].l) p=t[p].l;
ans=p;break;
}
if(t[p].val>val&&t[p].val<t[ans].val) ans=p;
p=val<t[p].val? t[p].l:t[p].r;
}
return t[ans].val;
} inline int getpre(int val){
int p=root,ans=1;
while(p){
if(t[p].val==val){
if(!t[p].l) break;
p=t[p].l;while(t[p].r) p=t[p].r;
ans=p;break;
}
if(t[p].val<val&&t[p].val>t[ans].val) ans=p;
p=val<t[p].val? t[p].l:t[p].r;
}
return t[ans].val;
}

然而,如果只有这些操作,那set岂不是可以替代手写平衡树?

所以,我们还有两个操作其实在BST里面就有的只是忘记讲了qwq

寻找一个值val的排名(只需要找已经插入的节点)

首先,我们可以知道,排名就是比它小的值的个数\(+1\),

而我们在寻找这个值\(val\)所在的节点时,有三种情况:

1.当前节点的权值大于\(val\).

此时,我们只需要向左走就行了.

2.当前节点的权值等于\(val\).

那么这时候,就直接返回它左子树的节点数量\(size\).

3.当前节点的权值小于\(val\).

那么显然,当前节点及它的左子树的权值都小于\(val\),

于是我们加上左子树的\(size\)以及当前节点的元素个数\(cnt\)(针对于重复元素),

再往右找即可.

来看代码吧:

inline int getrank(int p,int val){
if(t[p].val==val) return t[t[p].l].size;
if(val<t[p].val) return getrank(t[p].l,val);
return t[t[p].l].size+t[p].cnt+getrank(t[p].r,val);
}

但是我们要注意的一点是,

我们最后返回的,是小于\(val\)的元素个数,因此要再加一,

但由于插入了一个\(-INF\)(避免越界),又要再减掉一(就等于没变,但原理必须要清楚).

让我们进入到第二个操作:

寻找排名为rank的元素

其实这个的思路和前面的也差不多...(并且和权值线段树很像)

还是三种情况:

1.当前节点左子树的元素个数大于等于\(rank\).

那么显然,答案在左子树中,因此往左边走就行了.

2.左子树元素个数\(size\)加上当前节点的重复元素个数\(cnt\)大于等于\(rank\).

那么当前节点的权值就是答案了.

3.左子树元素个数\(size\)加上当前节点的重复元素个数\(cnt\)小于\(rank\).

那么答案就在右子树中啦,但是要将\(rank\)减掉左子树元素个数\(size\)加上当前节点的重复元素个数\(cnt\)

那么看代码吧:

inline int getval(int p,int rank){
if(t[t[p].l].size>=rank) return getval(t[p].l,rank);
if(t[t[p].l].size+t[p].cnt>=rank) return t[p].val;
return getval(t[p].r,rank-t[t[p].l].size-t[p].cnt);
}

好吧,几个操作讲完啦!!!

来看道例题吧:洛谷P3369 【模板】普通平衡树

这题就是个板子了.

直接上代码吧:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#define INF 0x7fffffff
using namespace std; inline int read(){
int sum=0,f=1;char c=getchar();
while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
while(c<='9'&&c>='0'){sum=sum*10+c-'0';c=getchar();}
return sum*f;
} struct tree{int l,r,size,cnt,val,dat;}t[100001];
int n,tot,root; inline int New(int val){
t[++tot].val=val;t[tot].dat=rand();
t[tot].size=t[tot].cnt=1;
return tot;
} inline void update(int p){
t[p].size=t[t[p].l].size+t[t[p].r].size+t[p].cnt;
} inline void l_rotate(int &p){
int q=t[p].l;
t[p].l=t[q].r;t[q].r=p;p=q;
update(t[p].r);update(p);
} inline void r_rotate(int &p){
int q=t[p].r;
t[p].r=t[q].l;t[q].l=p;p=q;
update(t[p].l);update(p);
} inline void build(){
New(-INF);New(INF);
t[1].r=2;root=1;
update(1);
} inline void insert(int &p,int val){
if(!p){p=New(val);return ;}
if(t[p].val==val) t[p].cnt++;
else insert(val<t[p].val? t[p].l:t[p].r,val);
if(t[t[p].l].dat>t[p].dat) l_rotate(p);
if(t[t[p].r].dat>t[p].dat) r_rotate(p);
update(p);
} inline void remove(int &p,int val){
if(!p) return ;
if(t[p].val==val){
if(t[p].cnt>1){t[p].cnt--;update(p);return ;}
if(!t[p].l&&!t[p].r){p=0;return ;}
if(!t[p].r||t[t[p].l].dat>t[t[p].r].dat) l_rotate(p),remove(t[p].r,val);
else r_rotate(p),remove(t[p].l,val);
update(p);return ;
}
remove(val<t[p].val? t[p].l:t[p].r,val);update(p);
} inline int getnext(int val){
int p=root,ans=2;
while(p){
if(t[p].val==val){
if(!t[p].r) break;
p=t[p].r;while(t[p].l) p=t[p].l;
ans=p;break;
}
if(t[p].val>val&&t[p].val<t[ans].val) ans=p;
p=val<t[p].val? t[p].l:t[p].r;
}
return t[ans].val;
} inline int getpre(int val){
int p=root,ans=1;
while(p){
if(t[p].val==val){
if(!t[p].l) break;
p=t[p].l;while(t[p].r) p=t[p].r;
ans=p;break;
}
if(t[p].val<val&&t[p].val>t[ans].val) ans=p;
p=val<t[p].val? t[p].l:t[p].r;
}
return t[ans].val;
} inline int getrank(int p,int val){
if(t[p].val==val) return t[t[p].l].size;
if(val<t[p].val) return getrank(t[p].l,val);
return t[t[p].l].size+t[p].cnt+getrank(t[p].r,val);
} inline int getval(int p,int rank){
if(t[t[p].l].size>=rank) return getval(t[p].l,rank);
if(t[t[p].l].size+t[p].cnt>=rank) return t[p].val;
return getval(t[p].r,rank-t[t[p].l].size-t[p].cnt);
} int main(){
n=read();build();
for(int i=1;i<=n;i++){
int opt=read(),x=read();
if(opt==1){insert(root,x);}
else if(opt==2){remove(root,x);}
else if(opt==3){printf("%d\n",getrank(root,x));}
else if(opt==4){printf("%d\n",getval(root,x+1));}//因为有一个-INF所以要加一
else if(opt==5){printf("%d\n",getpre(x));}
else if(opt==6){printf("%d\n",getnext(x));}
}
return 0;
}

Treap终于讲完啦.

等着更splay吧...(或许坑填不上了)

[学习笔记] 平衡树——Treap的更多相关文章

  1. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  2. [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家

    1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...

  3. 学习笔记--(平衡树)splay

    坑爹的splay,毁我青春,耗我钱财,颓我精力 是一种用于保存有序集合的简单高效的数据结构.伸展树实质上是一个二叉查找树.允许查找,插入,删除,删除最小,删除最大,分割,合并等许多操作,这些操作的时间 ...

  4. 平衡树学习笔记(2)-------Treap

    Treap 上一篇:平衡树学习笔记(1)-------简介 Treap是一个玄学的平衡树 为什么说它玄学呢? 还记得上一节说过每个平衡树都有自己的平衡方式吗? 没错,它平衡的方式是......rand ...

  5. 普通平衡树Treap(含旋转)学习笔记

    浅谈普通平衡树Treap 平衡树,Treap=Tree+heap这是一个很形象的东西 我们要维护一棵树,它满足堆的性质和二叉查找树的性质(BST),这样的二叉树我们叫做平衡树 并且平衡树它的结构是接近 ...

  6. 平衡树学习笔记(6)-------RBT

    RBT 上一篇:平衡树学习笔记(5)-------SBT RBT是...是一棵恐怖的树 有多恐怖? 平衡树中最快的♂ 不到200ms的优势,连权值线段树都无法匹敌 但是,通过大量百度,发现RBT的代码 ...

  7. 平衡树学习笔记(5)-------SBT

    SBT 上一篇:平衡树学习笔记(4)-------替罪羊树 所谓SBT,就是Size Balanced Tree 它的速度很快,完全碾爆Treap,Splay等平衡树,而且代码简洁易懂 尤其是插入节点 ...

  8. 平衡树学习笔记(3)-------Splay

    Splay 上一篇:平衡树学习笔记(2)-------Treap Splay是一个实用而且灵活性很强的平衡树 效率上也比较客观,但是一定要一次性写对 debug可能不是那么容易 Splay作为平衡树, ...

  9. BST,Splay平衡树学习笔记

    BST,Splay平衡树学习笔记 1.二叉查找树BST BST是一种二叉树形结构,其特点就在于:每一个非叶子结点的值都大于他的左子树中的任意一个值,并都小于他的右子树中的任意一个值. 2.BST的用处 ...

随机推荐

  1. 基于hanlp的es分词插件

    摘要:elasticsearch是使用比较广泛的分布式搜索引擎,es提供了一个的单字分词工具,还有一个分词插件ik使用比较广泛,hanlp是一个自然语言处理包,能更好的根据上下文的语义,人名,地名,组 ...

  2. 【ActiveReports 大数据分析报告】2019国庆旅游出行趋势预测

    今年国庆假期全国接待国内游客人数有望达到8亿人次! 随着2019国庆小长假的临近,不少游客已经开始着手规划假期出游路线.据权威机构发布的<2019国庆旅游趋势预测报告>显示,今年“十一黄金 ...

  3. 关于mac配置vs code的C++环境问题

    在配置完成后,编译通过了但是在终端输出一直不出现很奇怪,求问大家这是啥问题 以下是我的配置

  4. Linux_目录基本操作_常用命令【详解】

    Linux_常用命令 Linux文件系统的目录树结构:[Linux世界里一切皆文件]:说白了,就是文件和文件夹(目录)之间的操作. 普通用户kkb所有文件及文件夹,其实都位于root用户的 /home ...

  5. 红米K20PRO解锁Bootloader权限并刷入recovery

    手机里反正没什么东西了,聊天记录啊好像也没很重要得了,索性全部清除,刷机玩玩. 把稳定版刷成第三方开发版,这样又有时间去折腾root权限,面具和xposed的各种插件了,嘿嘿. 解锁小米手机 我的账号 ...

  6. 通过using声明改变个别成员的可访问性

    C++的语法中通过在派生类中使用using声明可以忽略继承方式 , 而让派生类对于基类的私有和保护成员具有特殊的访问权限 , 甚至可以改变派生类对象对于基类成员的访问权限 . 个人认为这种语法很容易让 ...

  7. Spring 注解介绍

    @Component与@Bean的区别 @Component注解表明一个类会作为组件类,并告知Spring要为这个类创建bean. @Bean注解告诉Spring这个方法将会返回一个对象,这个对象要注 ...

  8. hdu4706

    #include<string.h> #include<stdio.h> int main() { int a,b,c,d,i,j,n,m; ][]; ,j=; a<=; ...

  9. Codeforces 1236C. Labs

    传送门 注意到 $f(X,Y)+f(Y,X)$ 是一个定值(因为每个元素都不相同) 所以如果能让 $f(X,Y)$ 与 $f(Y,X)$ 尽可能接近,那么一定是最优的 所以可以这样构造:把 $n^2$ ...

  10. Open API

    OAuth和SSO都可以做统一认证登录,但是OAuth的流程比SSO复杂.SSO只能做用户的认证登录,OAuth不仅能做用户的认证登录,开可以做open api开放更多的用户资源. Open API即 ...