Treap(平衡树)
Treap
前置芝士
二叉搜索树(BST),见 BST。
平衡二叉树(AVL)。
先来介绍一下平衡二叉树。
背景
BST 出现以后,人们很快发现一个问题,当其维护一个有序序列时,很可能会退化成链。如图:
这样的话,原来 \(O(\log{n})\) 的复杂度就退化为 \(O(n)\),这是我们无法接受的,于是平衡二叉树横空出世。
定义
平衡二叉树:左右子树的高度相差不超过 1 的 BST(可以为空树)。平衡,顾名思义,就是要求左右子树的高度相近。
下面给出一些图,请判断是否为平衡二叉树:
显然,只有第二棵树是平衡二叉树,第一棵树节点 5 左右子树不平衡,第三棵树不是 BST。
基础知识
treap,就是“树堆”,由树和堆组成,是一种入门级的平衡二叉树,操作较多,码量较大,但比较基础,好理解。
\]
二叉搜索树(BST)对于一个序列来说不唯一,也就是说,在满足“BST性质”的前提下,中序遍历为相同序列的 BST 不唯一。因此,在 BST 的基础上加上二叉堆,来保证平衡性。用来维护 BST 的值为“关键码”,维护堆的值为权值,权值是随机产生的,避免退化。维护堆性质的操作为“旋转”。Treap 是一种通过适当的旋转,在维持节点关键码满足 BST 性质的同时,还使每个节点上随机生成的权值满足二叉堆的性质的平衡二叉树。 各个操作时间复杂度为 \(O(\log{n})\)。
基本操作有:
- 插入一个数 \(x\)。
- 删除一个数 \(x\)(若有多个相同的数,应只删除一个)。
- 定义排名为比当前数小的数的个数 \(+1\)。查询 \(x\) 的排名。
- 查询数据结构中排名为 \(x\) 的数。
- 求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
- 求 \(x\) 的后继(后继定义为大于 \(x\),且最小的数)。
操作
建树
与 BST 相同,建立一棵空树,不过我们需要储存更多的信息,size 为以该节点为根的子树大小,cnt 表示序列中该关键字的个数,pushup 和线段树的一样,更新父亲的信息。
const int N=1e5+5,inf=1<<30;
struct treap
{
int l,r;
int key,dat;//关键字,附加权值
int size,cnt;//子树大小,副本数
}a[N];
int tot,root,n,m;
int New(int k)
{
a[++tot].key=v;
a[tot].dat=rand();
a[tot].cnt=a[tot].size=1;
return tot;
}
void pushup(int u)
{
a[u].size=a[a[u].l].size+a[a[u].r].size+a[u].cnt;
}
void build()
{
New(-inf),New(inf);
root=1,a[root].r=2;
}
旋转
旋转是 Treap 的基本操作,分为左旋和右旋。如图:
- 右旋:把父亲变为左儿子的右儿子。
- 左旋:把父亲变为右儿子的左儿子。
如图,对于黑色节点来说,左边右旋后,该节点的左子树节点数减一,右子树加一,右边左旋后,该节点的左子树节点树加一,右子树减一。也就是说对于一个节点右旋,会增加右子树的节点数,左旋会增加左子树的节点数,利用左旋和右旋我们就可以维护二叉平衡树。
以右旋为例,将 \(y\) 变为 \(x\) 的右儿子,对于 \(x\) 左儿子不变,右儿子变为 \(y\),这样 \(y\) 的左儿子就空出来了,刚好可以把 \(x\) 的右子树 \(B\) 接上去。具体:\(y\) 的左子树变为 \(B\),\(x\) 的右儿子变为 \(y\),\(x\) 代替 \(y\) 原来的位置。
左旋同理:
void zig(int &u)//右旋
{
int q=a[u].l;
a[u].l=a[q].r,a[q].r=u,u=q;
pushup(a[u].r),pushup(u);
}
void zag(int &u)//左旋
{
int q=a[u].r;
a[u].r=a[q].l,a[q].l=u,u=q;
pushup(a[u].l),pushup(u);
}
当权值不满足大根堆(小也可)性质时,交换父亲和儿子,这样就能使 BST 更平衡。
注意要用 &
,这样的话会一起更新它父亲的信息,可以认为,\(q\) 完全代替了 \(u\)。
插入
将 \(key\) 为 \(v\) 的数插入到平衡树中,若原本已经存在,则直接找到位置将 \(cnt\) 加上 1。若不存在,则需将这个数插入到叶子节点,一直向下走直到找到要插入的位置,插入后判断是否满足大根堆性质,进行旋转来维护。
void insert(int &u,int v)
{
if(u==0) u=New(v);//u指向新的节点
else if(a[u].key==v) a[u].cnt++;
else if(a[u].key>v)
{
insert(a[u].l,v);
if(a[u].dat<a[a[u].l].dat) zig(u);//不满足大根堆,交换左儿子和父亲
}
else
{
insert(a[u].r,v);
if(a[u].dat<a[a[u].r].dat) zag(u);//不满足大根堆,交换右儿子和父亲
}
pushup(u);
}
删除
将 \(key\) 为 \(v\) 的数从平衡树中删除,与插入类似,先要找到该节点。若该节点的 \(cnt\) 大于 1,直接减去 1 即可。若小于 1,考虑如何删除,如果该点是叶子节点那就好办了,直接用 0 代替,但如果不是,我们也不能直接删,要通过不断地旋转把它变成叶子节点,具体看代码,自己手推一遍就理解了。
void remove(int &u,int v)
{
if(u==0) return;//没有这个数
if(a[u].key==v)
{
if(a[u].cnt>1) a[u].cnt--;
else if(a[u].l&&a[u].r)//非叶子节点
{
//保证旋转过后能满足大根堆的性质,哪个大就把哪个作为父亲
if(a[u].r==0||a[a[u].l].dat>a[a[u].r].dat) zig(u),remove(a[u].r,v);//右旋后父亲变为右儿子
else zag(u),remove(a[u].l,v)
}
else u=0;
}
else a[u].key>v ? remove(a[u].l,v) : remove(a[u].r,v);//一直往下走
pushup(u);
}
查排名
\(v\) 的排名为小于它的个数 \(+1\)。考虑 BST 中,比当前节点小的点应该全部位于左子树,因此排名就是左子树的大小 \(+1\)。所以先找到该值,再算个数。考虑如何从根节点往下走:
- \(key==v\),说明已找到,直接返回左子树的大小加 1。
- \(key>v\),则需往左子树走。
- \(key<v\),则需往右子树走,同时加上左子树大小和该节点副本数。
- \(u==0\),说明树中不存在 \(v\) 的节点,直接加 1。
需要在递归出口加上 1。
int get_rank(int u,int v)
{
if(u==0) return 1;
if(a[u].key==v) return a[a[u].l].size+1;
if(a[u].key>v) return get_rank(a[u].l,v);
return get_rank(a[u].r,v)+a[a[u].l].size+a[u].cnt;
}
查值
已知排名为 \(k\),查询具体的值。大同小异:
- 若左子树大小大于等于 \(k\),说明 \(k\) 在左子树,往左子树走。
- 若 \(k\) 大于左子树大小且小于左子树大小加副本数,说明该节点就是答案。
- 若 \(k\) 大于左子树大小加副本数,说明在右子树,往右子树继续找。
int get_val(int u,int k)
{
if(a[a[u].l].size>=k) return get_val(a[u].l,k);
if(a[a[u].l].size+a[u].cnt>=k) return a[u].key;
return get_val(a[u].r,k-a[a[u].l].size-a[u].cnt);//减去比k小的值的个数
}
查前驱/后继
前驱定义为小于 \(x\),且最大的数,后继定义为大于 \(x\),且最小的数。
以前驱为例,一直往下走,不满足 \(key<x\) 则往左子树走,否则开始找最大值。
int get_pre(int u,int v)
{
if(u==0) return -inf;
if(a[u].key>=v) return get_pre(a[u].l,v);
return max(a[u].key,get_pre(a[u].r,v));
}
int get_ne(int u,int v)
{
if(u==0) return inf;
if(a[u].key<=v) return get_ne(a[u].r,v);
return min(a[u].key,get_ne(a[u].l,v));
}
P3369 【模板】普通平衡树
分析
将以上讲的所有操作结合在一起就好了,注意细节。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define lc a[u].l
#define rc a[u].r
#define val a[u].key
const int N=1e5+5,inf=1<<30;
struct treap
{
int l,r;
int key,dat;
int size,cnt;
}a[N];
int tot,root,n,m;
int New(int v)
{
a[++tot].key=v;
a[tot].dat=rand();
a[tot].cnt=a[tot].size=1;
return tot;
}
void pushup(int u)
{
a[u].size=a[lc].size+a[rc].size+a[u].cnt;
}
void build()
{
New(-inf),New(inf);
root=1,a[root].r=2;
}
void zig(int &u)//右旋
{
int q=lc;
lc=a[q].r,a[q].r=u,u=q;
pushup(rc),pushup(u);
}
void zag(int &u)//左旋
{
int q=rc;
rc=a[q].l,a[q].l=u,u=q;
pushup(lc),pushup(u);
}
void insert(int &u,int v)
{
if(u==0) u=New(v);
else if(val==v) a[u].cnt++;
else if(val>v)
{
insert(lc,v);
if(a[lc].dat>a[u].dat) zig(u);//不满足大根堆,交换左儿子和父亲
}
else
{
insert(rc,v);
if(a[rc].dat>a[u].dat) zag(u);//交换右儿子
}
pushup(u);
}
void remove(int &u,int v)
{
if(u==0) return ;
if(val==v)
{
if(a[u].cnt>1) a[u].cnt--;
else if(lc||rc) //有叶子节点
{
//保证旋转过后能满足大根堆的性质,哪个大就把哪个作为父亲
if(rc==0||a[lc].dat>a[rc].dat) zig(u),remove(rc,v);//右旋后父亲变为右儿子
else zag(u),remove(lc,v);
pushup(u);
}
else u=0;
}
else val>v ? remove(lc,v) : remove(rc,v);
pushup(u);
}
int get_rank(int u,int v)
{
if(u==0) return 1;
if(val==v) return a[lc].size+1;
if(val>v) return get_rank(lc,v);
return get_rank(rc,v)+a[lc].size+a[u].cnt;
}
int get_val(int u,int k)
{
if(a[lc].size>=k) return get_val(lc,k);
if(a[lc].size+a[u].cnt>=k) return val;
return get_val(rc,k-a[lc].size-a[u].cnt);
}
int get_pre(int u,int v)
{
if(u==0) return -inf;
if(val>=v) return get_pre(lc,v);
return max(val,get_pre(rc,v));
}
int get_ne(int u,int v)
{
if(u==0) return inf;
if(val<=v) return get_ne(rc,v);
return min(val,get_ne(lc,v));
}
int main ()
{
cin>>m;
build();
for(int op,x;m--;)
{
cin>>op>>x;
if(op==1) insert(root,x);
else if(op==2) remove(root,x);
else if(op==3) cout<<get_rank(root,x)-1<<"\n";//减去-inf的点
else if(op==4) cout<<get_val(root,x+1)<<"\n";//存在-inf
else if(op==5) cout<<get_pre(root,x)<<"\n";
else cout<<get_ne(root,x)<<"\n";
}
return 0;
}
结语
有了 Treap,时间复杂度不会退化了,但是——这代码也太长了。而 FHQ_Treap 和 Splay 就会更好 QAQ。
Treap(平衡树)的更多相关文章
- BZOJ3786星系探索——非旋转treap(平衡树动态维护dfs序)
题目描述 物理学家小C的研究正遇到某个瓶颈. 他正在研究的是一个星系,这个星系中有n个星球,其中有一个主星球(方便起见我们默认其为1号星球),其余的所有星球均有且仅有一个依赖星球.主星球没有依赖星球. ...
- BZOJ3159决战——树链剖分+非旋转treap(平衡树动态维护dfs序)
题目描述 输入 第一行有三个整数N.M和R,分别表示树的节点数.指令和询问总数,以及X国的据点. 接下来N-1行,每行两个整数X和Y,表示Katharon国的一条道路. 接下来M行,每行描述一个指令或 ...
- BZOJ3729Gty的游戏——阶梯博弈+巴什博弈+非旋转treap(平衡树动态维护dfs序)
题目描述 某一天gty在与他的妹子玩游戏.妹子提出一个游戏,给定一棵有根树,每个节点有一些石子,每次可以将不多于L的石子移动到父节点,询问将某个节点的子树中的石子移动到这个节点先手是否有必胜策略.gt ...
- POJ 2985 Treap平衡树(求第k大的元素)
这题也能够用树状数组做,并且树状数组姿势更加优美.代码更加少,只是这个Treap树就是求第K大元素的专家--所以速度比較快. 这个也是从那本红书上拿的模板--自己找了资料百度了好久,才理解这个Trea ...
- treap平衡树
今天集训讲平衡树,就瞎搞了一下.直接下代码. #include<iostream> #include<cstdio> #include<cmath> #includ ...
- 算法模板——平衡树Treap
实现功能如下——1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个)3. 查询x数的排名(若有多个相同的数,因输出最小的排名)4. 查询排名为x的数5. 求x的前驱(前驱定义为小于x,且最大 ...
- 普通平衡树Treap(含旋转)学习笔记
浅谈普通平衡树Treap 平衡树,Treap=Tree+heap这是一个很形象的东西 我们要维护一棵树,它满足堆的性质和二叉查找树的性质(BST),这样的二叉树我们叫做平衡树 并且平衡树它的结构是接近 ...
- Day2平衡树笔记
线段树不支持的操作:删除,插入 常见的平衡树 treap 慢||好写 sbt(大小平衡的树) 非常快 比较好写 ||功能不全 rbt 红黑树 特别快 || 非常难写 以上操作支持插入删除O(Nlo ...
- N.O.W,O.R,N.E.V.E.R--12days to LNOI2015
双向链表 单调队列,双端队列 单调栈 堆 带权并查集 hash 表 双hash 树状数组 线段树合并 平衡树 Treap 随机平衡二叉树 Scapegoat Tree 替罪羊树 朝鲜树 块状数组,块状 ...
- 『这是一篇干货blog』
更新记录一些很好的干货博客以及工具网站. 各文章,工具网站版权归原作者所有,侵删. Articles 浅谈C++ IO优化--读优输优方法集锦 浅谈斜率优化 思维导图好助手--开心食用Xmind Ty ...
随机推荐
- Verilog HDL数据流建模与运算符
数据流建模使用的连续赋值语句由关键词assign开始,一般用法如下: wire [位宽说明]变量名1, 变量名2, ..., 变量名n; assign 变量名 = 表达式; 只要等号右边的值发生变化, ...
- AdaBoost算法解密:从基础到应用的全面解析
本文全面而深入地探讨了AdaBoost算法,从其基础概念和原理到Python实战应用.文章不仅详细解析了AdaBoost的优缺点,还通过实例展示了如何在Python中实现该算法. 关注TechLead ...
- java中ArrayList和LinkedList的区别
Java中ArrayList和LinkedList都是List集合的实现类,它们都可以用来存储一组有序的元素,但是它们的内部实现方式不同,在使用时也有不同的适用场景. ArrayList是一个基于动态 ...
- python中pip下载慢或报错的解决方法
一:问题 python的pip在安装包时,有时会报错超时,排除包名写错的原因,一般这种问题是因为网络下载过慢,导致超时 二:解决方案 我们可以设置pip镜像源下载,能够提升pip下载速度,解决报错问题 ...
- 25 个超棒的 Python 脚本合集
Python是一种功能强大且灵活的编程语言,拥有广泛的应用领域.下面是一个详细介绍25个超棒的Python脚本合集: 1. 网络爬虫:使用Python可以轻松编写网络爬虫,从网页中提取数据并保存为结构 ...
- LeetCode-Java:122. 买卖股票的最佳时机Ⅱ
题目 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格. 在每一天,你可以决定是否购买和/或出售股票.你在任何时候 最多 只能持有 一股 股票.你也可以先购买, ...
- 429 You are being rate limited
记录贴 429 真的很让人伤心 清除浏览器数据 我用的 Edge : 设置 ⇒ 隐私.搜索和服务 ⇒ 清除浏览器数据 ⇒ 立即清除 然后就重新登陆可以了
- Java核心知识体系8:Java如何保证线程安全性
Java核心知识体系1:泛型机制详解 Java核心知识体系2:注解机制详解 Java核心知识体系3:异常机制详解 Java核心知识体系4:AOP原理和切面应用 Java核心知识体系5:反射机制详解 J ...
- AntDesignBlazor示例——分页查询
本示例是AntDesign Blazor的入门示例,在学习的同时分享出来,以供新手参考. 示例代码仓库:https://gitee.com/known/BlazorDemo 1. 学习目标 分页查询框 ...
- Feign源码解析:初始化过程(一)
前言 打算系统分析下Feign的代码,上一篇讲了下Feign的历史,本篇的话,先讲下Feign相关的beanDefinition,beanDefinition就是bean的设计图,bean都是按照be ...