平衡树
二叉搜索树(Binary Search tree,BST)
二叉搜索树是所有平衡树的基础
定义
二叉搜索树是一种树状结构,并满足BST性质:
- 空树是一颗二叉搜索树。
- 若左子树不为空,则左子树上的所有节点的权值都比根节点小。
- 若右子树不为空,则右子树上的所有节点的权值都比根节点大。
- 二叉搜索树的左右子树均满足上述性质。
用途
由于二叉搜索树满足BST性质,可以做到快速添加元素、查询元素,查询元素排名,求前驱/后继,删除元素等。
实现
建树(build)
使用结构体struct保存每个节点的左子树、右子树,以及该节点的权值,结构体外保存节点数,根。
如果你觉得结构体太垃圾也可以用数组
struct BST{
int l,r;//左右子树
int val; //权值
}T[NUM];
int stot,rot // 总结点数,根节点
当需要建立新节点时:
int New(int val){
T[++stot].val=val;
return stot;
}
为避免越界,通常在建树时预先建立两个节点 \(-inf\) 以及 \(inf\),分别代表极小值与极大值。
下面是建树的实现:
struct BST{
int l,r;
int val;
}T[NUM];
int stot,root;
int New(int val){
T[++stot].val=val;
return stot;
}
void build(){
root=New(-inf);
T[root].r=New(inf);
}
建完的树长这样:

插入(insert)
假设要插入权值为 \(val\) 的节点:
- 若当前节点为空,新建一个节点。
- 若 \(val\) 大于当前节点权值,前往右子树。
- 若 \(val\) 小于当前节点权值,前往左子树。
实现时引用根节点,新建一个节点后其父节点信息可以同步更改:
void insert(int val,int &p=root){
if(!p) return p=New(val),void();
if(val==T[p].val) return;
if(val>T[p].val) insert(val,T[p].r);
else insert(val,T[p].l);
}
调用入口:
insert(val);
查找(find)
假设要查找权值为 \(val\) 的节点:
- 若当前节点权值等于 \(val\),则返回当前节点。
- 若不存在当前节点,说明权值为 \(val\) 的节点不存在。
- 若 \(val\) 大于当前节点权值,查找右子树。
- 若 \(val\) 小于当前节点权值,查找左子树。
思路非常简单,代码也是:
int find(int val,int p=root){
if(!p) return -inf;
if(val==T[p].val) return p;
return val>T[p].val?find(val,T[p].r):find(val,T[p].l);
}
调用入口:
find(val);
前驱/后继(prev/next)
定义
\(val\) 的前驱: 在二叉搜索树中,权值小于 \(val\) 的最大权值。
\(val\) 的后继: 在二叉搜索树中,权值大于 \(val\) 的最小权值。
求法
前驱(prev)
设 \(val\) 的前驱为 \(ans\):
- 先让 \(ans\) 为 \(-inf\) 的节点,然后查找 \(val\)。
- 在查找过程中,若当前节点的权值小于 \(val\) 那么说明当前节点可能成为 \(ans\),这时候将 \(val\) 与 \(ans\) 所对应的权值比较,取最大值。
- 若查找到 \(val\),且其对应的节点有左子树,那么 \(ans\) 就应该为其左子树中最靠右的点(即权值最大的点)。
- 若没有左子树或没找到 \(val\) 对应的节点,此时的 \(ans\) 即为答案。
后继(next)
与求前驱差不多,设 \(val\) 的前驱为 \(ans\):
- 先让 \(ans\) 为 \(inf\) 的节点,然后查找 \(val\)。
- 在查找过程中,若当前节点的权值大于于 \(val\) 那么说明当前节点可能成为 \(ans\),这时候将 \(val\) 与 \(ans\) 所对应的权值比较,取最小值。
- 若查找到 \(val\),且其对应的节点有右子树,那么 \(ans\) 就应该为其右子树中最靠左的点(即权值最小的点)。
- 若没有右子树或没找到 \(val\) 对应的节点,此时的 \(ans\) 即为答案。
下面解释一下
以求前驱为例:
我们可以发现,一个节点的右子树上的点的权值肯定比当前的大,而前驱的定义是权值小于 \(val\) 的最大权值,那么当前节点一定是 \(ans\) 的一个候选,这时应当将当前节点的权值与 \(ans\) 进行比较,取权值更大的一项。
当找到 \(val\) 时,若当前节点有左子树,为什么答案就是左子树中最靠右的点呢?想一下为什么这点能成为左子树?因为它比 \(val\) 小。为什么能成为左子树中最靠右的点?因为它是这一堆权值比 \(val\) 小的节点中最大的一个,这不就是 \(val\) 的前驱了吗!
代码
int getpre(int val){
int ans=1,p=root;
while(p){
if(val==T[p].val){
if(T[p].l>0){
p=T[p].l;
while(T[p].r>0) 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 ans;
}
int getnext(int val){
int ans=2,p=root;
while(p){
if(val==T[p].val){
if(T[p].r>0){
p=T[p].r;
while(T[p].l>0) 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 ans;
}
为什么这个代码长得这么丑
注意这里我们返回的是节点的编号!!!
删除(remove)
删除比较麻烦,因为删除一个节点后还得保证这是一个二叉搜索树,具体步骤如下:
设要删除权值为 \(val\) 的节点。
- 先查找 \(val\),记为 \(p\);
- 若 \(p\) 的子节点数小于等于 \(1\),直接删除,并令其子节点替代其位置。
- 若 \(p\) 的子节点数大于 \(1\),查找 \(val\) 的后继 \(next\) ,令 \(next\) 的右子树替代\(next\) 位置,令 \(next\) 替代 \(p\),删除 \(p\)。
可以证明:若 \(p\) 的子节点数大于 \(1\),那么后继 \(next\) 没有左子树,否则此 \(next\) 不是 \(val\) 的后继。
void remove(int val,int &p=root){
if(!p) return;
if(val==T[p].val){
if(T[p].l==0){
p=T[p].r;
}else if(T[p].r==0){
p=T[p].l;
}else{
int nxt=T[p].r;
while(T[nxt].l) nxt=T[nxt].l;
remove(T[nxt].val,T[p].r);
T[nxt].l=T[p].l,T[nxt].r=T[p].r;
p=nxt;
}
return;
}
if(val>T[p].val) remove(val,T[p].r);
else remove(val,T[p].l);
}
查 \(val\) 的排名(getrank)
需要在结构体中额外维护一个值 \(siz\) 表示以当前节点为根的子树的节点个数,那么我们可以写出pushup函数:
void pushup(int p){
T[p].size=T[T[p].l].size+T[T[p].r].size+1;
}
其中 \(+1\) 表示加上当前节点大小。
每次进行有关节点的添加、删除操作后,对根(当前子树的根)进行pushup即可。
于是我们就可以写出查询排名的操作步骤:
当到达一个节点 \(p\) 时
- 若 \(p\) 不存在,返回 \(0\)(有时需要返回 \(1\))。
- 如果 \(val\) 大于 \(p\) 的权值,说明 \(p\) 的左子树的权值都比 \(val\) 小,答案要加上 \(p\) 左子树的 \(siz\) 以及 \(p\) 这一节点,继续向 \(p\) 的右子树查找 \(val\)。
- 如果 \(val\) 小于 \(p\) 的权值,无法确定是否能对答案造成贡献,继续向左子树查找 \(val\)。
- 若查找到 \(val\),返回 \(p\) 左子树的 \(siz+1\)。
关于 \(p\) 不存在返回 \(1\) 还是 \(0\) 的问题,假设 \(val\) 不存在于树中:
- 返回 \(1\) 表示在这颗树中第一个比 \(val\) 大的值的排名。
- 返回 \(0\) 表示在这颗树中第一个比 \(val\) 小的值的排名。
int getrank(int val,int p=root){
if(!p) return 0;
if(T[p].val==val) return T[T[p].l].size+1;
if(T[p].val>val) return getrank(val,T[p].l);
return T[T[p].l].size+1+getrank(val,T[p].r);
}
有不懂的看着看着就懂了
还要注意一点不要忘了有一个权值为 \(-inf\) 的节点!!!
查排名为 \(rank\) 的值(getval)
类似查排名。
当到达一个节点 \(p\) ,记其左子树大小为 \(siz_l\):
- 若 \(p\) 不存在返回一个极大值(不存在的排名)。
- 若 \(siz_l\) 大于等于 \(rank\),继续在左子树中查找排名为 \(rank\) 的值。
- 若 \(siz_l+1\) 等于 \(rank\),返回 \(p\) 的权值,即为答案。
- 若 \(siz_l\) 小于 \(rank\),继续在右子树中查找排名为 \(rank-siz_l-1\) 的节点。
int getval(int rank,int p=root){
if(p==0) return INF;
if(T[T[p].l].size>=rank) return getval(rank,T[p].l);
if(T[T[p].l].size+1==rank) return T[p].val;
return getval(rank-T[T[p].l].size-1,T[p].r);
}
还要注意一点不要忘了有一个权值为 \(-inf\) 的节点!!!
对于可重集合
可以额外维护一个 \(cnt\),记录每个元素出现的次数。
下面是一些更改:
void pushup(int p){
T[p].size=T[T[p].l].size+T[T[p].r].size+T[p].cnt;
}
int getrank(int val,int p=root){
if(!p) return 0;
if(T[p].val==val) return T[T[p].l].size+1;
if(T[p].val>val) return getrank(val,T[p].l);
return T[T[p].l].size+T[p].cnt+getrank(val,T[p].r);
}
int getval(int rank,int p=root){
if(p==0) return INF;
if(T[T[p].l].size>=rank) return getval(rank,T[p].l);
if(T[T[p].l].size+T[p].cnt>=rank) return T[p].val;
return getval(rank-T[T[p].l].size-T[p].cnt,T[p].r);
}
缺点
想象一下,当我们一次将一个升序序列插入,那么我们得到的将不会是一颗树,也就是说,在特殊构造的数据中,二叉搜索树会退化为一条链。

那么对这棵树操作的复杂度就会退化到 \(O(n)\),这是我们不能接受的,这称之为这棵二叉搜索树失去了平衡。
平衡树
顾名思义,平衡树就是一颗平衡的二叉搜索树,平衡树右很多种,每一种都有其独特的保持平衡的方式。
Treap(树堆/笛卡尔树)
Treap是一种特殊的笛卡尔树。
Treap 是一种弱平衡的二叉搜索树,又称树堆,是一种结合了二叉搜索树和堆的数据结构,它除了要维护每个点的权值(val),还要维护每个点的优先级(priority),其中权值满足二叉搜索树性质,优先级满足堆性质。
对于二叉搜索树性质,前文已经给出,堆性质指的是:
- 子节点的优先级比父节点大或小(取决于是大根堆还是小根堆)
其中优先级随机给出,目的是通过随机化,打乱原本的不平衡形态。
虽然有点扯,但这还真有用。
struct BST{
int l,r;
int val,pty;
}T[NUM];
int stot,root;
int New(int val){
T[++stot].val=val;
T[stot].pty=rand();
return stot;
}
有旋Treap
通过旋转维护平衡,主要分为右旋与左旋。
旋转(zig/zag)

记当前节点为 \(p\)。
其中右旋为:
- 将 \(p\) 的左节点的右节点作为 \(p\) 的左节点,将 \(p\) 作为其左节点的右节点,并将 \(p\) 的左节点放在 \(p\) 的位置。
其中左旋为:
- 将 \(p\) 的右节点的左节点作为 \(p\) 的右节点,将 \(p\) 作为其右节点的左节点,并将 \(p\) 的右节点放在 \(p\) 的位置。
说白了就是绕口令
看不懂没关系,图看懂了就行。
下面是旋转代码:
void zig(int &p){
int q=T[p].l;
T[p].l=T[q].r,T[q].r=p,p=q;
}
void zag(int &p){
int q=T[p].r;
T[p].r=T[q].l,T[q].l=p,p=q;
}
代码
由于加入了旋转等操作,有旋Treap与传统二叉搜索树的写法有了些许改变:
- 插入一个新节点时,需要判断树是否满足堆性质,否则应通过旋转来使其保持堆性质。
- 删除一个节点时,不需要再去寻找前驱后继,只需将要删除的节点旋转到叶节点直接删除即可。
下面给出完整有旋Treap代码:
struct打包
const int NUM=1e7;
const int inf=2147483647;
struct tree{
int l,r;
int val,dat;
int cnt,size;
}T[NUM];
int tot;
struct Treap{
int root;
int New(int val){
T[++tot].val=val;
T[tot].dat=rand();
T[tot].cnt=T[tot].size=1;
return tot;
}
void update(int p){
T[p].size=T[T[p].l].size+T[T[p].r].size+T[p].cnt;
}
void build(int &p){
p=New(-inf),New(inf);
T[p].r=New(inf);
update(p);
}
int getrank(int p,int val){
if(!p) return 1;
if(T[p].val==val) return T[T[p].l].size+1;
if(T[p].val>val) return getrank(T[p].l,val);
return T[T[p].l].size+T[p].cnt+getrank(T[p].r,val);
}
int getval(int p,int rank){
if(p==0) return inf;
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);
}
void zig(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);
}
void zag(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);
}
void insert(int &p,int val){
if(p==0){
p=New(val);
return;
}
if(val==T[p].val){
T[p].cnt++;
update(p);
return;
}
if(val<T[p].val){
insert(T[p].l,val);
if(T[p].dat<T[T[p].l].dat) zig(p);
}else{
insert(T[p].r,val);
if(T[p].dat<T[T[p].r].dat) zag(p);
}
update(p);
}
int getpre(int val){
int ans=1,p=root;
while(p){
if(val==T[p].val){
if(T[p].l>0){
p=T[p].l;
while(T[p].r>0) 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 ans;
}
int getnext(int val){
int ans=2,p=root;
while(p){
if(val==T[p].val){
if(T[p].r>0){
p=T[p].r;
while(T[p].l>0) 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 ans;
}
void remove(int &p,int val){
if(p==0) return;
if(val==T[p].val){
if(T[p].cnt>1) return T[p].cnt--,update(p),void();
if(T[p].l||T[p].r){
if(T[p].r==0||T[T[p].l].dat>T[T[p].r].dat){
zig(p),remove(T[p].r,val);
}else{
zag(p),remove(T[p].l,val);
}
update(p);
}
else p=0;
return;
}
val<T[p].val?remove(T[p].l,val):remove(T[p].r,val);
update(p);
}
};
namespace打包
namespace Treap{
const int NUM=1e6+10;
struct tree{
int l,r;
int val,dat;
int cnt,size;
}T[NUM];
int tot,root,INF=0x3f3f3f3f;
int New(int val){
T[++tot].val=val;
T[tot].dat=rand();
T[tot].cnt=T[tot].size=1;
return tot;
}
void update(int p){
T[p].size=T[T[p].l].size+T[T[p].r].size+T[p].cnt;
}
void build(){
New(-INF),New(INF);
root=1,T[1].r=2;
update(root);
}
int getrank(int val,int p=root){
if(!p) return 1;
if(T[p].val==val) return T[T[p].l].size+1;
if(T[p].val>val) return getrank(val,T[p].l);
return T[T[p].l].size+T[p].cnt+getrank(val,T[p].r);
}
int getval(int rank,int p=root){
if(p==0) return INF;
if(T[T[p].l].size>=rank) return getval(rank,T[p].l);
if(T[T[p].l].size+T[p].cnt>=rank) return T[p].val;
return getval(rank-T[T[p].l].size-T[p].cnt,T[p].r);
}
void zig(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);
}
void zag(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);
}
void insert(int val,int &p=root){
if(p==0){
p=New(val);
return;
}
if(val==T[p].val){
T[p].cnt++;
update(p);
return;
}
if(val<T[p].val){
insert(val,T[p].l);
if(T[p].dat<T[T[p].l].dat) zig(p);
}else{
insert(val,T[p].r);
if(T[p].dat<T[T[p].r].dat) zag(p);
}
update(p);
}
int getpre(int val){
int ans=1,p=root;
while(p){
if(val==T[p].val){
if(T[p].l>0){
p=T[p].l;
while(T[p].r>0) 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 ans;
}
int getnext(int val){
int ans=2,p=root;
while(p){
if(val==T[p].val){
if(T[p].r>0){
p=T[p].r;
while(T[p].l>0) 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 ans;
}
void remove(int val,int &p=root){
if(p==0) return;
if(val==T[p].val){
if(T[p].cnt>1) return T[p].cnt--,update(p),void();
if(T[p].l||T[p].r){
if(T[p].r==0||T[T[p].l].dat>T[T[p].r].dat){
zig(p),remove(val,T[p].r);
}else{
zag(p),remove(val,T[p].l);
}
update(p);
}
else p=0;
return;
}
val<T[p].val?remove(val,T[p].l):remove(val,T[p].r);
update(p);
}
}
using namespace Treap;
无旋Treap(FHQ-Treap)
可以说是最简单、最好写哪里好写的平衡树,但代价就是常数大。
无旋Treap通过分裂、合并来保持平衡。
分裂(split)
无旋Treap分裂后会分裂成两个小Treap,在这里称之为左树和右树。
分裂时通常要有一个值 \(val\),将所有比 \(val\) 小或等于的节点放在左树上,将所有值比 \(val\) 大的放在右树上。
我们分别为左树右树创建一个虚拟节点 \(x\)、\(y\):
- 若当前节点的权值小于 \(val\),则说明说明当前节点与其左子树都比 \(val\) 小,当前节点与其左子树整个放入左树的虚拟节点中,并在当前节点的右子节点重新建立左树的建立虚拟节点。
- 若当前节点的权值大于 \(val\),则说明说明当前节点与其右子树都比 \(val\) 大,当前节点与其左子树整个放入右树的虚拟节点中,并在当前节点的左子节点重新建立右树的建立虚拟节点。
- 若当前节点为空,则说明没有新的节点了,直接删除左右树的虚拟节点。
字太多会不会有人不愿意看,其实1和2是一样的
这里引用别人的图。

代码有点让人云里雾里的,第一次我也没太看懂:
void split(int &x,int &y,int val,int p=root){
if(!p) return x=y=0,void();
if(T[p].val<=val) x=p,split(T[p].r,y,val,T[p].r);
else y=p,split(x,T[p].l,val,T[p].l);
pushup(p);
}
不会只有我把根写在了最后一个吧
稍微解释一下,这个实现比较神奇,它其实并没有建立一个建立一个虚拟节点,而是将 \(p\) 的子节点引用,当要在其上面加树的时候直接让其指向被加树的根节点,如果还不理解的话可以手动模拟一下。
合并(merge)
合并就比较简单了,设要讲以 \(u\) 为根的左树与以 \(v\) 为根的右树合并:
- 若 \(u\) 的优先级比 \(v\) 小,将 \(v\) 合并到 \(u\) 的右子树。
- 若 \(u\) 的优先级比 \(v\) 大,将 \(u\) 合并到 \(v\) 的左子树。
以上是小根堆的策略,大根堆反过来就行了。
int merge(int u,int v){
if(!u||!v) return u+v;
if(T[u].pty<T[v].pty){
T[u].r=merge(T[u].r,v);
return pushup(u),u;
}else{
T[v].l=merge(u,T[v].l);
return pushup(v),v;
}
}
代码
为什么说无旋Treap是最好写的平衡树?
有了分裂和合并操作,插入、删除、前驱、后继、排名等操作就会变得非常简单,下面以插入为例:
我们不在需要递归找到 \(val\) 的位置,只需要将整个树按 \(val\) 拆开,将左树与 \(val\) 合并,再让左树与右树合并即可。
这就导致了无旋Treap的 \(log\) 奇大无比
void insert(int val,int &p=root){
int x,y;
split(x,y,val,p);
p=merge(merge(x,New(val)),y);
}
其他操作同理,但查找排名还得按之前的方法。
下面是完整无旋Treap代码:
struct打包
const int NUM=1e7;
const int inf=2147483647;
struct tree{
int l,r;
int val,pty;
int siz,cnt;
}T[NUM];
int stot;
struct Treap{
int root;
void pushup(int p){
T[p].siz=T[T[p].l].siz+T[T[p].r].siz+T[p].cnt;
}
void split(int p,int val,int &x,int &y){
if(!p) return x=y=0,void();
if(T[p].val<=val) x=p,split(T[p].r,val,T[p].r,y);
else y=p,split(T[p].l,val,x,T[p].l);
pushup(p);
}
int merge(int x,int y){
if(!x||!y) return x|y;
if(T[x].pty<T[y].pty){
T[x].r=merge(T[x].r,y);
return pushup(x),x;
}else{
T[y].l=merge(x,T[y].l);
return pushup(y),y;
}
}
int New(int val){
T[++stot].val=val;
T[stot].pty=rand();
T[stot].siz=T[stot].cnt=1;
return stot;
}
void insert(int val){
int x,y,z;
split(root,val,x,z);
split(x,val-1,x,y);
if(T[y].cnt) T[y].cnt++,T[y].siz++,root=merge(merge(x,y),z);
else root=merge(merge(x,New(val)),z);
}
void build(){
insert(-inf),insert(inf);
}
int find(int p,int val){
if(!p) return 0;
if(val==T[p].val) return p;
return val>T[p].val?find(T[p].r,val):find(T[p].l,val);
}
int getval(int p,int rank){
if(!p) return inf;
if(T[T[p].l].siz>=rank) return getval(T[p].l,rank);
if(T[T[p].l].siz+T[p].cnt>=rank) return T[p].val;
return getval(T[p].r,rank-T[T[p].l].siz-T[p].cnt);
}
int getrank(int val){
int x,y;
split(root,val-1,x,y);
int k=T[x].siz+1;
root=merge(x,y);
return k;
}
int getpre(int val){
int x,y;
split(root,val-1,x,y);
int ans=getval(x,T[x].siz);
root=merge(x,y);
return ans;
}
int getnext(int val){
int x,y;
split(root,val,x,y);
int ans=getval(y,1);
root=merge(x,y);
return ans;
}
void remove(int val){
int x,y,z;
split(root,val,x,z);
split(x,val-1,x,y);
if(T[y].cnt>1) T[y].cnt--,T[y].siz--;
else y=merge(T[y].l,T[y].r);
root=merge(merge(x,y),z);
}
};
namespace打包
namespace Treap{
const int NUM=1e6+10;
const int inf=0x3f3f3f3f;
struct tree{
int l,r;
int val,pty;
int siz,cnt;
}T[NUM];
int root,stot;
void pushup(int p){
T[p].siz=T[T[p].l].siz+T[T[p].r].siz+T[p].cnt;
}
void split(int &x,int &y,int val,int p=root){
if(!p) return x=y=0,void();
if(T[p].val<=val) x=p,split(T[p].r,y,val,T[p].r);
else y=p,split(x,T[p].l,val,T[p].l);
pushup(p);
}
int merge(int u,int v){
if(!u||!v) return u+v;
if(T[u].pty<T[v].pty){
T[u].r=merge(T[u].r,v);
return pushup(u),u;
}else{
T[v].l=merge(u,T[v].l);
return pushup(v),v;
}
}
int New(int val){
T[++stot].val=val;
T[stot].pty=rand();
T[stot].cnt=T[stot].siz=1;
return stot;
}
void insert(int val,int &p=root){
int x,y,z;
split(x,z,val,p);
split(x,y,val-1,x);
if(y) T[y].cnt++,T[y].siz++,p=merge(merge(x,y),z);
else p=merge(merge(x,New(val)),z);
}
void build(int &p=root){
insert(-inf,p),insert(inf,p);
}
int find(int val,int p=root){
if(!p) return 0;
if(val==T[p].val) return p;
return val>T[p].val?find(val,T[p].r):find(val,T[p].l);
}
int getval(int rank,int p=root){
if(p==0) return inf;
if(T[T[p].l].siz>=rank) return getval(rank,T[p].l);
if(T[T[p].l].siz+T[p].cnt>=rank) return T[p].val;
return getval(rank-T[T[p].l].siz-T[p].cnt,T[p].r);
}
int getrank(int val,int &p=root){
int x,y;
split(x,y,val-1,p);
int k=T[x].siz+1;
p=merge(x,y);
return k;
}
int getpre(int val,int &p=root){
int x,y;
split(x,y,val-1,p);
int tmp=getval(T[x].siz,x);
p=merge(x,y);
return tmp;
}
int getnext(int val,int &p=root){
int x,y;
split(x,y,val,p);
int tmp=getval(1,y);
p=merge(x,y);
return tmp;
}
void remove(int val,int &p=root){
int x,y,z;
split(x,z,val,p);
split(x,y,val-1,x);
if(T[y].cnt>1) T[y].cnt--,T[y].siz--,p=merge(merge(x,y),z);
else y=merge(T[y].l,T[y].r),p=merge(merge(x,y),z);
}
}
using namespace Treap;
Splay树(伸展树)
Splay 是一种自平衡二叉查找树,由 Daniel Sleator 和 Robert Tarjan 没错又是他与1985年发明,它用过伸展操作不断将某个节点旋转到根节点来保持平衡。
旋转(rotate)
为什么不是左旋和右旋?
其实就是左旋右旋,只是Splay在在实现的时候将这俩打包成了一个函数,于是就可以理解为将一个节点转上去(转到其父节点的位置)所以是不是可以叫上旋,这就需要额外维护父节点信息 \(fa\) ,以及为了方便,将左右子节点放在了数组中维护(方便取下标)。
struct tree{
int s[2],f;
int val,siz,cnt;
}T[NUM];
记当前节点为 \(x\),\(y\) 为 \(x\) 的父节点,\(z\) 为 \(y\) 的父节点:
- 将 \(x\) 挂在 \(z\) 上。
- 将 \(x\) 的子节点挂在 \(y\) 上。
- 将 \(y\) 挂在 \(x\) 上。
云里雾里!我到底在写什么
我们一条一条说:
将 \(x\) 挂在 \(z\) 上:
显然不知道哪条边挂在哪条边上
我们得先知道 \(x\) 要挂在 \(z\) 的那个节点上,要挂在 \(z\) 连 \(y\) 的那个地方,那么我们怎么能知道 \(y\) 是在 \(z\) 的左边还是右边呢?
还记得前面说过的一句话吗:
为了方便,将左右子节点放在了数组中维护(方便取下标)。
于是聪明的你就想到了一个绝妙的方法:
T[z].s[1]==y;
这是一个bool类型的表达式,看看如果 \(y\) 是 \(z\) 的右节点,这个式子的值是什么?\(1\)!那如果 \(y\) 是 \(z\) 的左节点呢?\(0\)!这不就是 \(y\) 所在的节点吗恍然大悟。
这样就可以写出来第一条的代码:
T[z].s[T[z].s[1]==y]=x;
T[x].f=z;
将 \(x\) 的子节点挂在 \(y\) 上:
显然x哪个子节点挂在y上
这个问题其实很好想:
- 如果 \(x\) 是 \(y\) 的左节点,那么就将 \(x\) 的右节点挂在 \(y\) 的左节点上。
- 如果 \(x\) 是 \(y\) 的右节点,那么就将 \(x\) 的左节点挂在 \(y\) 的右节点上。
我们记 \(k\) 为 \(x\) 在 \(y\) 节点的位置,\(k=0\) 表示 \(x\) 是 \(y\) 的左节点, \(x=1\) 表示 \(x\) 是 \(y\) 的右节点,于是我们可以写出第二部分代码:
T[y].s[k]=T[x].s[k^1];
T[T[x].s[k^1]].f=y;
将 \(y\) 挂在 \(x\) 上:
这个简单,\(x\) 的哪个节点挂在了 \(y\) 上,就 \(y\) 挂在哪个节点上:
T[x].s[k^1]=y;
T[y].f=x;
下面是旋转部分的代码:
void rotate(int x){
int y=T[x].f;
int z=T[y].f;
int k=T[y].s[1]==x;
T[z].s[T[z].s[1]==y]=x;
T[x].f=z;
T[y].s[k]=T[x].s[k^1];
T[T[x].s[k^1]].f=y;
T[x].s[k^1]=y;
T[y].f=x;
pushup(y),pushup(x);
}
伸展(splay)
伸展便是Splay的精髓其实也是旋转,说人话就是将要查的节点直接转到根节点然后就可以为所欲为了。
伸展时需要考虑两种情况:
- 一字形:
\(x\)、\(y\)、\(z\) 程一字型分布时,先将 \(y\) 转到 \(z\) 的位置,再将 \(x\) 转上来。 - 之字形:
\(x\)、\(y\)、\(z\) 程之字型分布时,先将 \(x\) 转到 \(y\) 的位置,再将 \(x\) 转上来。
代码实现过程中,若要将 \(x\) 转到 \(k\) 的子节点位置,特别地,若 \(k=0\) 则是旋转到根节点,可以不断地取 \(x\) 的父节点 \(y\) 与祖父节点 \(z\) 若 \(z\) 不是 \(k\) 就按上述方式进行旋转,否则就直接将 \(x\) 旋转到 \(y\)。
最后,如果 \(k=0\) 记得把根换成 \(x\)。
void splay(int x,int k){
while(T[x].f!=k){
int y=T[x].f;
int z=T[y].f;
if(z!=k){
if((T[y].s[1]==x)^(T[z].s[1]==y)){
rotate(x);
}else rotate(y);
}
rotate(x);
}
if(!k) root=x;
}
以下未完工......

浙公网安备 33010602011771号