前言

学完了替罪羊树,我决定再去学一学\(Treap\)。一直听说\(Treap\)很难,我也花了挺久才学会。

简介

\(Treap\)这个名字真的挺有内涵:

\(\color{red}{Tree}\)+\(\color{blue}{Heap}\)=\(\color{red}{Tre}\)+\(\color{blue}{eap}\)=\(\color{red}{Tr}\color{purple}{e}\color{blue}{ap}\)

这很形象地告诉了我们:\(Treap\)是\(Tree\)(二叉搜索树)与\(Heap\)(堆)的结合体,这也是\(Treap\)能够平衡的关键。

\(Treap\)平衡的方法

\(Treap\)为什么能够平衡?它与普通的\(BST\)有什么区别?

主要就在于,它比\(BST\)多了一个优先级的设定。

首先,\(Treap\)的节点是满足\(BST\)性质的。其次,\(Treap\)的每一个节点还有一个优先级,而这些优先级又是满足堆性质的。这就能让\(Treap\)的节点保持一种随机的状态,而不会被数据卡成链(当然,脸黑也没办法)。

至于如何让它同时满足\(BST\)性质和堆性质,这放在后面再讲。

那么,该怎么确定优先级呢?

很简单,随机即可。

不过,直接用\(C++\)自带的\(rand()\)又慢又容易被卡,所以推荐手写\(rand()\),下面的代码仅供参考:

inline int Rand()
{
static ull r=2333;//static不能少,r的初值可以自己定
return (r*=233333)%=2147483647;//每次r乘上的数也可以自己定
}

\(Treap\)的基础操作

  • 旋转

    旋转应该是\(Treap\)最重要也是最核心的操作了。

    \(Treap\)的旋转分为两种:左旋右旋

    我们以左旋为例:

如图,是一棵需要左旋的\(Treap\)(圆圈中是节点编号而不是节点权值)。

首先,我们把根节点的右儿子改为根节点的右儿子的左儿子(右旋恰好相反)。

然后,再将根节点的右儿子的左儿子改为根节点,然后再将根节点改为原根节点的右儿子即可(右旋刚好相反)。

以上就是\(Treap\)的旋转操作了,但是在代码中的操作要略麻烦一点:

inline void Rotate(int &x,int d)//由于左旋和右旋的操作恰好相反,且为了避免写两段代码,我们用d=0表示左旋,d=1表示右旋
{
int k=node[x].Son[d^1];//由于左旋时用根节点的右儿子,右旋时用根节点的左儿子,所以用k存储的要与旋转方向相反
node[x].Son[d^1]=node[k].Son[d],node[k].Son[d]=x,x=k,PushUp(node[x].Son[d]),PushUp(x);//将根的与旋转方向反向的儿子改为k的与旋转方向同向的儿子,然后将k的与旋转方向同向的儿子改为根节点,并将根节点改为k,最后要记得更新节点信息
}
  • 插入

    \(Treap\)的插入操作与一般的\(BST\)有点像,只不过每次插入后都要比较当前节点与其儿子的优先级,从而决定是否要旋转。

    代码如下:

inline void Insert(int &x,int val)//插入操作
{
if(!x) {x=Build(val);return;}//如果当前节点为空,就将元素插入这个节点
++node[x].Size;//将当前元素所在的子树大小加1
if(node[x].Val==val) ++node[x].Cnt;//如果当前元素等于插入元素,就将当前元素的个数加1
else if(node[x].Val>val)//否则如果当前元素大于插入元素
{
Insert(node[x].Son[0],val);//将插入元素插入当前元素的左子树
if(node[x].Data<node[node[x].Son[0]].Data) Rotate(x,1);//如果当前元素左儿子的优先级大于当前节点的优先级,那么就右旋(将左儿子旋到当前节点之上)
}
else//与上面类似
{
Insert(node[x].Son[1],val);
if(node[x].Data<node[node[x].Son[1]].Data) Rotate(x,0);
}
PushUp(x);//最后记得更新当前元素的信息
}
  • 删除

    删除\(Treap\)上的一个元素,我们要分几种情况讨论:

    • 若删除元素原本存在多个,则只需将其存在个数减\(1\)即可。
    • 否则,若删除元素没有子节点,则直接将这个元素赋值为\(0\)即可。
    • 否则,若删除元素没有右子节点,或左子节点的优先级高于右子节点,就将以删除元素为根的子树右旋,我们可以发现,此时:原先的左子节点\(-->\)新的根节点,原先的根节点\(-->\)新的右子节点,因此,我们只要继续删除新的右子节点即可。
    • 否则,就将以删除元素为根的子树左旋,继续删除新的左子节点即可(与上面类似)。

    代码如下:

inline void Delete(int &x,int val)//删除值为val的数
{
if(!x) return;//如果当前节点为空节点,就退出函数
if(node[x].Val==val)//如果当前节点为要删除的节点,那就进行删除操作
{
if(node[x].Cnt>1) {--node[x].Cnt,PushUp(x);return;}//如果删除元素存在多个,就将其个数减1
if(node[x].Son[0]||node[x].Son[1])//如果当前元素有子节点
{
if(!node[x].Son[1]||node[node[x].Son[0]].Data>node[node[x].Son[1]].Data) Rotate(x,1),Delete(node[x].Son[1],val);//若删除元素没有右子节点,或左子节点的优先级高于右子节点,就将以删除元素为根的子树右旋,继续删除新的右子节点
else Rotate(x,0),Delete(node[x].Son[0],val);//否则,就将以删除元素为根的子树左旋,继续删除新的左子节点
}
else x=0;//如果当前元素没有子节点,直接将这个元素赋值为0
}
else if(node[x].Val>val) Delete(node[x].Son[0],val);//如果当前元素大于删除元素,说明删除元素在当前元素的左子树
else Delete(node[x].Son[1],val);//否则说明删除元素在当前元素的右子树
PushUp(x);//最后要记得更新当前节点的信息
}
  • 询问

    \(Treap\)的询问操作和普通\(BST\)没什么太大的区别,直接贴代码了:

inline int get_rank(int val)//询问值为val的数的排名
{
int x=rt,rk=0;
while(x)//只要当前节点不为空
{
if(node[x].Val==val) return node[node[x].Son[0]].Size+rk+1;//如果当前元素等于val,就可以直接返回答案了
else if(node[x].Val>val) x=node[x].Son[0];//如果当前元素大于v,则说明当前元素的排名大于val,所以访问当前元素的左子树
else rk+=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];//否则,将计数器加上当前元素的排名(不要忘记加上当前元素的存在个数),并访问当前元素的右子树
}
return rk;
}
inline int get_val(int rk)//询问排名为rk的数的值
{
int x=rt;
while(x)//只要当前节点不为空
{
if(node[node[x].Son[0]].Size>=rk) x=node[x].Son[0];//如果当前元素的排名等于rk,则返回该节点的值
else if(node[node[x].Son[0]].Size+node[x].Cnt>=rk) return node[x].Val;//否则,如果当前元素的排名大于rk,访问当前元素的左子树
else rk-=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];//不然,就将rk减去当前元素的排名,访问当前元素的右子树
}
}
inline int get_pre(int val)//询问值为val的数的前驱,也可以用get_val(get_rank(x)-1),只不过常数大一些
{
int x=rt,pre=0;
while(x)//只要当前节点不为空
{
if(node[x].Val<val) pre=node[x].Val,x=node[x].Son[1];//如果当前元素小于val,就将前驱改为当前元素,并访问当前元素的右子树
else x=node[x].Son[0];//否则,访问当前元素的左子树
}
return pre;
}
inline int get_nxt(int val)//询问值为val的数的后继,也可以用get_val(get_rank(x+1)),只不过常数大一些
{
int x=rt,nxt=0;
while(x)//只要当前节点不为空
{
if(node[x].Val>val) nxt=node[x].Val,x=node[x].Son[0];//如果当前元素大于val,就将后继改为当前元素,并访问当前元素的左子树
else x=node[x].Son[1];//否则,访问当前元素的右子树
}
return nxt;
}
  • 最后,还有一个细节:

初始化时要现在平衡树中加入一个极大值和一个极小值,防止越界。

完整代码

讲了这么多,最后来一个模板:(还是以【洛谷3369】【模板】普通平衡树为例吧)

#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define LL long long
#define swap(x,y) (x^=y,y^=x,x^=y)
#define tc() (A==B&&(B=(A=ff)+fread(ff,1,100000,stdin),A==B)?EOF:*A++)
#define pc(ch) (pp_<100000?pp[pp_++]=(ch):(fwrite(pp,1,100000,stdout),pp[(pp_=0)++]=(ch)))
#define N 100000
int pp_=0;char ff[100000],*A=ff,*B=ff,pp[100000];
using namespace std;
int n,rt,tot=0;
struct Treap
{
int Son[2],Val,Cnt,Size,Data;
}node[N+5];
inline void read(int &x)
{
x=0;int f=1;char ch;
while(!isdigit(ch=tc())) f=ch^'-'?1:-1;
while(x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc()));
x*=f;
}
inline void write(int x)
{
if(x<0) pc('-'),x=-x;
if(x>9) write(x/10);
pc(x%10+'0');
}
inline int Rand()
{
static LL r=2333;
return (r*=233333LL)%=2147483647;
}
inline void PushUp(int x)
{
node[x].Size=node[node[x].Son[0]].Size+node[node[x].Son[1]].Size+node[x].Cnt;
}
inline int Build(int val)
{
node[++tot].Val=val,node[tot].Cnt=node[tot].Size=1,node[tot].Son[0]=node[tot].Son[1]=0,node[tot].Data=Rand();
return tot;
}
inline void Init()
{
rt=Build(-2e9),node[rt].Son[1]=Build(2e9),PushUp(rt);
}
inline void Rotate(int &x,int d)
{
int k=node[x].Son[d^1];
node[x].Son[d^1]=node[k].Son[d],node[k].Son[d]=x,x=k,PushUp(node[x].Son[d]),PushUp(x);
}
inline void Insert(int &x,int val)
{
if(!x) {x=Build(val);return;}
++node[x].Size;
if(node[x].Val==val) ++node[x].Cnt;
else if(node[x].Val>val)
{
Insert(node[x].Son[0],val);
if(node[x].Data<node[node[x].Son[0]].Data) Rotate(x,1);
}
else
{
Insert(node[x].Son[1],val);
if(node[x].Data<node[node[x].Son[1]].Data) Rotate(x,0);
}
PushUp(x);
}
inline void Delete(int &x,int val)
{
if(!x) return;
if(node[x].Val==val)
{
if(node[x].Cnt>1) {--node[x].Cnt,PushUp(x);return;}
if(node[x].Son[0]||node[x].Son[1])
{
if(!node[x].Son[1]||node[node[x].Son[0]].Data>node[node[x].Son[1]].Data) Rotate(x,1),Delete(node[x].Son[1],val);
else Rotate(x,0),Delete(node[x].Son[0],val);
}
else x=0;
}
else if(node[x].Val>val) Delete(node[x].Son[0],val);
else Delete(node[x].Son[1],val);
PushUp(x);
}
inline int get_rank(int val)
{
int x=rt,rk=0;
while(x)
{
if(node[x].Val==val) return node[node[x].Son[0]].Size+rk+1;
else if(node[x].Val>val) x=node[x].Son[0];
else rk+=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];
}
return rk;
}
inline int get_val(int rk)
{
int x=rt;
while(x)
{
if(node[node[x].Son[0]].Size>=rk) x=node[x].Son[0];
else if(node[node[x].Son[0]].Size+node[x].Cnt>=rk) return node[x].Val;
else rk-=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];
}
}
inline int get_pre(int val)
{
int x=rt,pre=0;
while(x)
{
if(node[x].Val<val) pre=node[x].Val,x=node[x].Son[1];
else x=node[x].Son[0];
}
return pre;
}
inline int get_nxt(int val)
{
int x=rt,nxt=0;
while(x)
{
if(node[x].Val>val) nxt=node[x].Val,x=node[x].Son[0];
else x=node[x].Son[1];
}
return nxt;
}
int main()
{
register int i;
for(read(n),Init(),i=1;i<=n;++i)
{
int op,x;read(op),read(x);
switch(op)
{
case 1:Insert(rt,x);break;
case 2:Delete(rt,x);break;
case 3:write(get_rank(x)-1),pc('\n');break;
case 4:write(get_val(x+1)),pc('\n');break;
case 5:write(get_pre(x)),pc('\n');break;
case 6:write(get_nxt(x)),pc('\n');break;
}
}
return fwrite(pp,1,pp_,stdout),0;
}

简析平衡树(二)——Treap的更多相关文章

  1. 简析平衡树(四)——FHQ Treap

    前言 好久没码过平衡树了! 这次在闪指导的指导下学会了\(FHQ\ Treap\),一方面是因为听说它可以可持久化,另一方面则是因为听说它是真的好写. 简介 \(FHQ\ Treap\),又称作非旋\ ...

  2. 简析平衡树(三)——浅谈Splay

    前言 原本以为\(Treap\)已经很难了,学习了\(Splay\),我才知道,没有最难,只有更难.(强烈建议先去学一学\(Treap\)再来看这篇博客) 简介 \(Splay\)是平衡树中的一种,除 ...

  3. Linux VFS机制简析(二)

    Linux VFS机制简析(二) 接上一篇Linux VFS机制简析(一),本篇继续介绍有关Address space和address operations.file和file operations. ...

  4. SpringMVC源码情操陶冶-DispatcherServlet简析(二)

    承接前文SpringMVC源码情操陶冶-DispatcherServlet类简析(一),主要讲述初始化的操作,本文将简单介绍springmvc如何处理请求 DispatcherServlet#doDi ...

  5. 简析平衡树(一)——替罪羊树 Scapegoat Tree

    前言 平衡树在我的心目中,一直都是一个很高深莫测的数据结构.不过,由于最近做的题目的题解中经常出现"平衡树"这三个字,我决定从最简单的替罪羊树开始,好好学习平衡树. 简介 替罪羊树 ...

  6. HTTP协议简析(二)

    HTTP响应也包含四部分内容: 响应行: 协议版本:HTTP/1.1 状态码:200 状态描述:对状态码的说明 响应头:用来规范数据,常用的有: server:服务器信息 date:响应的时间 las ...

  7. 【ACM/ICPC2013】POJ基础图论题简析(一)

    前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...

  8. Linux VFS机制简析(一)

    Linux VFS机制简析(一) 本文主要基于Linux内核文档,简单分析Linux VFS机制,以期对编写新的内核文件系统(通常是给分布式文件系统编写内核客户端)的场景有所帮助. 个人渊源 切入正文 ...

  9. zxing二维码扫描的流程简析(Android版)

    目前市面上二维码的扫描似乎用开源google的zxing比较多,接下去以2.2版本做一个简析吧,勿喷... 下载下来后定位两个文件夹,core和android,core是一些核心的库,android是 ...

随机推荐

  1. game with probability problem

    两个人 A, B 取 n 枚石子,祂们轮流抛硬币 (A 先手),每次抛硬币,如果是正面,就取出一枚石子,否则什么都不做,然而 A, B 有一种超能力,在抛硬币前在意志中确定一面 (正面或反面),然后就 ...

  2. [Swift]Scanner字符串扫描类

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...

  3. 解决LINUX下SQLPLUS时上下左右键乱码问题

    window下的sqlplus可以通过箭头键,来回看历史命令,用起来非常的方便. 但是在linux里就没有这么方面了,错了一个命令,我们必须重新敲一次,辛苦了手指头叻. 看到一个文章,很方便的一招,给 ...

  4. Unity---遇到的一些坑和解决方案

    目录 1.在UGUI中的物体顺时针旋转Z是负的.(和正常3D中是相反的) 2.MoveTowards()+Vector3.Distance()控制物体的移动 3.trtransform.SetPare ...

  5. VC添加全局热键的方法

    VC添加全局热键的方法 这个方法靠谱 http://blog.csdn.net/lujianfeiccie2009/article/details/7498704 VC添加全局热键的方法 标签: bu ...

  6. P1984 [SDOI2008]烧水问题(具体证明)

    传送门 我见过的第二恶心的题,第一是糖果传递... 以下是一堆具体的证明,自己想的,可能考虑不周,不想看也可以直接看结论 首先有一个很显然的贪心,烧开的水要尽量把热量传递出去 所以有一个比较显然的方法 ...

  7. 在CMD下运用管理员权限

    方法一:鼠标右键 这个方法比较比较普通,点开开始找到cmd,右击鼠标“以管理员身份运行(A)”这样调用就是管理员的权限: 方法二:快捷模式 在点开win+R后,选择“以管理员身份运行”,然后确定:可以 ...

  8. Silverlight 密码框 Focus

    在做一个例子是需要运行起来后焦点默认设置在密码框上,在网上查了资料 自己找到一种方法,此方法在oob模式下管用 public Login() { InitializeComponent(); txtL ...

  9. 017 Letter Combinations of a Phone Number 电话号码的字母组合

    给定一个数字字符串,返回数字所有可能表示的字母组合. 输入:数字字符串 "23"输出:["ad", "ae", "af" ...

  10. Java匹马行天下之JavaWeb核心技术——JSP

    JSP动态网页技术 一.JavaWeb简介 一.什么是JavaWeb? JavaWeb是用Java技术来解决相关web互联网领域的技术总称. 需要在特定的web服务器上运行,分为web服务器和web客 ...