[OI] 平衡树
1. 二叉查找树
二叉查找树的思想和优先队列比较像,都是把若干个数据按一定规则插到一棵树里,然后就可以维护特定的信息.
在优先队列的大根堆实现里,我们让每棵子树的根节点都大于它的儿子,这样就可以保证根节点一定是那个最大值,也就是我们需要的最值操作.
那么二叉查找树,顾名思义是可以查找特定 \(rank\) 或类似 \(lower\_bound()\) 的元素的树,通常为了实现这个我们是这样定义的:
- 令左子节点小于父节点
- 令右子节点大于父节点
比较容易想到,这样我们维护一个 \(size\) 就能快速二分查找答案了.
但是二叉查找树对链式结构非常敏感,因此需要进行各种优化. 二叉查找树有不少优化统称为平衡树,但核心思想都是一样的.
那么是什么核心思想呢,首先我们可以注意到,假如我们对二叉查找树中序遍历,那么得到的序列一定是有序的,因此我们需要保证在优化结构的同时,还要保证中序遍历不变,否则就无法再查找答案了. 这就是平衡树的思想.
下面对每种平衡树进行具体展开.
2.Splay
发明 : \(\texttt{Daniel Sleator}\) and \(\texttt{Robert Tarjan}\) (1985)
P6136 [C++14 -O2] 平均耗时 \(24.9450\) s | [C++14] 平均耗时 \(24.6067\) s
2.1 算法思想
基本思路:将一个点的父节点移到它的子节点的子节点上
特点:跑得比较快,不好写,代码不好理解,总体来看不如 Treap,所以不太详细讲
下面我们设 \(r\ (root)\) 为 \(x\) 的父节点,并且 \(x\) 为左孩子. 可以发现,根据二叉平衡树的性质,一定会有 \(w_{r}\ge w_{x}\),因此我们应该将 \(r\) 放在 \(x\) 的右孩子位置.
那么假如 \(x\) 已经有了右孩子 \(s\) 怎么办呢,还是根据二叉平衡树的性质,应该有 \(w_{s}\le w_{r}\),因此我们把 \(s\) 直接放到 \(y\) 的左孩子上,因为 \(r\) 的左孩子是 \(x\) ,因此 \(r\) 现在一定没有左孩子了,正好可以放进去. 这样的操作我们称为左旋.
右旋类似,只不过 \(x\) 变成了 \(r\) 的右孩子.
现在我们又有了两个新的问题:如何利用这些操作优化结构?如何实现这种优化?
Splay 对于每个修改/查询的节点,都首先把它翻到根节点的位置,这样来保证频繁查找的节点距离根节点最近. 可以证明,Splay 的修改/查询的复杂度均摊 \(log\ n\).
但是实际上我们会发现,在把 \(x\) 旋转到根节点的过程中,树的结构完全没有得到优化,这是因为我们仅仅旋转了 \(x\),假设 \(x\) 的祖先是 \(y\),\(y\) 的祖先是 \(z\),假如现在 \(x\) 是 \(y\) 的左子树,\(y\) 是 \(x\) 的左子树,那么无论如何旋转 \(x\),树的深度是永远也不会变的,所以我们需要特殊处理这种情况,来让树的深度减小一点.
注意到我们可以先旋转 \(y\),强行把 \(x\) 和 \(z\) 拉到同一个深度,这样就可以继续旋转 \(x\) 了,这就是 Splay 的基本思想.
2.2 核心代码
2.2.1 更新
目标:统计更新节点 \(x\) 的子树大小
inline void update(int x){
if(x){
t[x].size=t[x].cnt;
if(t[x].son[0]) t[x].size+=t[t[x].son[0]].size;
if(t[x].son[1]) t[x].size+=t[t[x].son[1]].size;
}
}
2.2.2 旋转
目标:对 \(x\) 执行一次旋转操作,使其与其父节点 \(y\) 交换位置.
基本步骤:
- 确认要旋转的点 \(x\) 是左孩子还是右孩子
- 按上述规律旋转 \(x\)(将 \(y\) 变为 \(x\) 异侧儿子,并将 \(x\) 被替换的孩子换到 \(y\) 的同侧)
inline void rotate(int x){
int f=t[x].fa,gf=t[f].fa;
int k=judgeson(x);
//judgeson():左孩子返回0,否则返回1
t[f].son[k]=t[x].son[k^1];
//k^1 也可用 1-k 代替
t[t[x].son[k^1]].fa=f;
t[x].son[k^1]=f;
t[f].fa=x;
t[x].fa=gf;
if(gf){
t[gf].son[t[gf].son[1]==f]=x;
}
update(f);update(x);
//记得 update
}
2.2.3 Splay
目标:将 \(x\) 旋转到根节点.
基本步骤:
- 反复对 \(x\) 执行旋转操作,直到 \(x\) 为根节点.
- 特殊地,若 \(x,y,z\ (y=father_{x},z=father_{y})\) 满足上述条件(\(x\) 是 \(y\) 的左子树,\(y\) 是 \(x\) 的左子树,或同为右子树),那么先旋转 \(y\),再旋转 \(x\).
inline void splay(int x){
for(int f;f=t[x].fa;rotate(x)){
//f=t[x].fa 这句实际上是 f=t[x].fa 和 f!=0 合起来
if(t[f].fa){
//这里没必要再else了,t[f].fa=0的情况一定会在下一次被跳出去
if(judgeson(x)==judgeson(f)){
//特殊情况
rotate(f);
}
else rotate(x);
}
}
root=x;
}
2.2.4 插入
目标:插入一个值为 \(x\) 的元素
基本步骤:
- 若树为空,新建节点并设置树根.
- 否则从树根开始查找元素 \(x\),若找到则该元素数量加一,否则新建一个节点.
inline void insert(int x){
if(!root){
//树为空
tot++;
t[tot]={x,1,1,0,{0,0}};
root=tot;
return;
}
int now=root,fa=0;
while(1){
if(x==t[now].w){
//找到元素x
t[now].cnt++;
update(now);update(fa);
splay(now);
break;
}
fa=now;
now=t[now].son[t[now].w<x];
//利用二叉平衡数的特殊性质跳转
if(!now){
//始终未找到x,新建节点
tot++;
t[tot]={x,1,1,fa,{0,0}};
t[fa].son[t[fa].w<x]=tot;
update(fa);
splay(tot);
break;
}
}
}
2.2.5 查询元素
目标:查找 \(rank\) 值为 \(x\) 的数
基本步骤:
- 根据二叉平衡树的性质,将 \(x\) 不断减去当前节点的左子树大小(左子树节点的 \(rank\) 一定更小)来逼近答案
- 直到逼近成负数,返回当前值
- 特殊地,若到达根节点,需要手动跳出,防止死循环
inline int findnum(int x){
int now=root;
while(now){
if(t[now].son[0] and x<=t[t[now]son[0]].size){
//这里必须要判,不然会多减
now=t[now].son[0];
}
else{
//减不动了再判断
int temp=t[now].cnt;
if(t[now].son[0]){
temp+=t[t[now].son[0]].size;
}
if(x<=temp) return t[now].w;
x-=temp;
now=t[now].son[1];
}
}
return t[now].w;
//特殊处理
}
2.2.6 查询 \(Rank\)
目标:查找值为 \(x\) 的元素的 \(rank\)
基本步骤:
- 先跳到值 \(x\) 的位置上,路上不断统计左子树大小(即 \(rank\) 比当前数小的数的数量)
- 特殊地,若跳到根节点,需要手动退出
- 更特殊地,本函数统计的是小于元素 \(x\) 的元素个数,请依题目描述适当更改
inline int findrank(int x){
int now=root,ans=0;
while(now){
if(x<t[now].w){
now=t[now].son[0];
//先尽可能往小跳
}
else{
//跳不动了开始处理
if(t[now].son[0]){
ans+=t[t[now].son[0]].size;
//累加答案
}
if(x==t[now].w){
splay(now);
return ans;
}
ans+=t[now].cnt;
now=t[now].son[1];
}
}
return ans;
//特殊处理
}
2.2.7 查询前驱
目标:查找第一个比元素 \(x\) 小的元素(即左子树的最右子树)
inline int findpre(){
int now=t[root].son[0];
while(t[now].son[1]) now=t[now].son[1];
return now;
}
2.2.8 查询后缀
目标:查找第一个比元素 \(x\) 大的元素(即右子树的最左子树)
inline int findnext(){
int now=t[root].son[1];
while(t[now].son[0]) now=t[now].son[0];
return now;
}
2.2.9 删除节点
目标:删除值为 \(x\) 的元素中的其中一个
基本步骤:
- 先把 \(x\) 跳到根节点
- 假如不止一个元素,那减掉一个即可
- 否则应该删掉这个点,删除后应该把当前节点的儿子与父亲全部转移到它的前驱节点上.
- 特殊地,对于该节点只有左儿子,只有右儿子或者都有,都没有的四种情况应分别讨论
inline void free(int x){
findrank(x);
if(t[root].cnt>1){
//不止一个
t[root].cnt--;
update(root);
return;
}
if(!t[root].son[0] and !t[root].son[1]){
//一个儿子也没有
clear(root);
root=0;
return;
}
if(!t[root].son[0]){
//只有右儿子
int oroot=root;
root=t[root].son[1];
t[root].fa=0;
clear(oroot);
return;
}
else if(!t[root].son[1]){
//只有左儿子
int oroot=root;
root=t[root].son[0];
t[root].fa=0;
clear(oroot);
return;
}
//全有
int left=findpre(),oroot=root;
splay(left);
t[root].son[1]=t[oroot].son[1];
t[t[oroot].son[1]].fa=root;
clear(oroot);
update(root);
}
2.3 完整代码
2.3.1 版本一 (功能不全)
namespace splay{
const int N=1000001;
#define tol(id) t[t[id].son[0]]
#define tor(id) t[t[id].son[1]]
int root,tot;
struct tree{
int w;
int tot,size;
int fa,son[2];
}t[N];
inline void update(int id){
t[id].size=tol(id).size+tor(id).size+t[id].tot;
}
inline void rotate(int x){
int y=t[x].fa,z=t[y].fa;
bool rs;
if(t[y].son[0]==x) rs=0;
else rs=1;
t[z].son[t[z].son[1]==y]=x;
t[x].fa=z;
t[y].son[rs]=t[x].son[rs^1];
t[t[x].son[rs^1]].fa=y;
t[x].son[rs^1]=y;
t[y].fa=x;
update(y);update(x);
}
inline void splay(int x,int k){
while(t[x].fa!=k){
int y=t[x].fa,z=t[y].fa;
if(z!=k){
if((t[z].son[0]==y)^(t[y].son[0]==x)){
rotate(x);
}
else rotate(y);
}
rotate(x);
}
if(k==0) root=x;
}
inline void find(int x){
int u=root;
if(!u) return;
while(t[u].son[x>t[u].w] and x!=t[u].w){
u=t[u].son[x>t[u].w];
}
splay(u,0);
}
inline void insert(int x){
int u=root,f=0;
while(u and t[u].w!=x){
f=u;
u=t[u].son[x>t[u].w];
}
if(u) t[u].tot++;
else{
u=++tot;
if(f) t[f].son[x>t[f].w]=u;
t[u].son[0]=t[u].son[1]=0;
t[tot]={x,1,1,f,{0,0}};
}
splay(u,0);
}
inline int findnext(int x,bool isnext){
find(x);
int u=root;
if(t[u].w>x and isnext) return u;
if(t[u].w<x and !isnext) return u;
u=t[u].son[isnext];
while(t[u].son[isnext^1]) u=t[u].son[isnext^1];
return u;
}
inline void free(int x){
int last=findnext(x,0);
int next=findnext(x,1);
if(last) splay(last,0);
splay(next,last);
int del=t[next].son[0];
if(t[del].tot>1){
t[del].tot--;
splay(del,0);
}
else t[next].son[0]=0;
}
inline int find(int x){
int u=root,ans=0;
while(u){
if(x<t[u].w) u=t[u].son[0];
else{
if(t[u].son[0]) ans+=t[t[u].son[0]].size;
if(x==t[u].w){
splay(u,0);
return ans+1;
}
ans+=t[u].tot;
u=t[u].son[1];
}
}
return ans+1;
}
}
2.3.2 版本二
#include<bits/stdc++.h>
using namespace std;
namespace splay{
const int N=1000001;
#define tol(i) t[(i)].son[0]
#define tor(i) t[(i)].son[1]
#define to(i,j) t[(i)].son[(j)]
struct splaytree{
int root,tot;
struct tree{
int w;
int cnt,size;
int fa,son[2];
}t[N];
inline void clear(int x){
t[x]={0,0,0,0,{0,0}};
}
inline bool judgeson(int x){
return t[t[x].fa].son[1]==x;
}
inline void update(int x){
if(x){
t[x].size=t[x].cnt;
if(t[x].son[0]) t[x].size+=t[t[x].son[0]].size;
if(t[x].son[1]) t[x].size+=t[t[x].son[1]].size;
}
}
inline void rotate(int x){
int f=t[x].fa,gf=t[f].fa;
int k=judgeson(x);
t[f].son[k]=t[x].son[k^1];
t[t[f].son[k]].fa=f;
t[x].son[k^1]=f;
t[f].fa=x;
t[x].fa=gf;
if(gf){
t[gf].son[t[gf].son[1]==f]=x;
}
update(f);update(x);
}
inline void splay(int x){
for(int f;f=t[x].fa;rotate(x)){
if(t[f].fa){
if(judgeson(x)==judgeson(f)){
rotate(f);
}
else rotate(x);
}
}
root=x;
}
inline void insert(int x){
if(!root){
tot++;
t[tot]={x,1,1,0,{0,0}};
root=tot;
return;
}
int now=root,fa=0;
while(1){
if(x==t[now].w){
t[now].cnt++;
update(now);update(fa);
splay(now);
break;
}
fa=now;
now=t[now].son[t[now].w<x];
if(!now){
tot++;
t[tot]={x,1,1,fa,{0,0}};
t[fa].son[t[fa].w<x]=tot;
update(fa);
splay(tot);
break;
}
}
}
inline int findnum(int rank){
int now=root;
while(now){
if(t[now].son[0] and rank<=t[t[now].son[0]].size){
now=t[now].son[0];
}
else{
int temp=t[now].cnt;
if(t[now].son[0]){
temp+=t[t[now].son[0]].size;
}
if(rank<=temp) return t[now].w;
rank-=temp;
now=t[now].son[1];
}
}
return t[now].w;
}
inline int findrank(int val){
int now=root,ans=0;
while(now){
if(val<t[now].w){
now=t[now].son[0];
}
else{
if(t[now].son[0]){
ans+=t[t[now].son[0]].size;
}
if(val==t[now].w){
splay(now);
return ans;
}
ans+=t[now].cnt;
now=t[now].son[1];
}
}
return ans;
}
inline int findpre(){
int now=t[root].son[0];
while(t[now].son[1]) now=t[now].son[1];
return now;
}
inline int findnext(){
int now=t[root].son[1];
while(t[now].son[0]) now=t[now].son[0];
return now;
}
inline int findpre(int val){
insert(val);
int ans=findpre();
free(val);
return ans;
}
inline int findnext(int val){
insert(val);
int ans=findnext();
free(val);
return ans;
}
inline int got(int id){
return t[id].w;
}
inline void free(int x){
findrank(x);
if(t[root].cnt>1){
t[root].cnt--;
update(root);
return;
}
if(!t[root].son[0] and !t[root].son[1]){
clear(root);
root=0;
return;
}
if(!t[root].son[0]){
int oroot=root;
root=t[root].son[1];
t[root].fa=0;
clear(oroot);
return;
}
else if(!t[root].son[1]){
int oroot=root;
root=t[root].son[0];
t[root].fa=0;
clear(oroot);
return;
}
int left=findpre(),oroot=root;
splay(left);
t[root].son[1]=t[oroot].son[1];
t[t[oroot].son[1]].fa=root;
clear(oroot);
update(root);
}
};
}
using namespace splay;
2.3.3 版本三:完善版
#include<bits/stdc++.h>
using namespace std;
namespace splay{
const int N=1000001;
int w[N];
const int inf=114514191;
struct splaytree{
int root,tot;
vector<int> search_answer;
struct tree{
int w;
int cnt,size;
int fa,son[2];
int tag;
}t[N];
inline void clear(int x){
t[x]={0,0,0,0,{0,0}};
}
inline bool judgeson(int x){
return t[t[x].fa].son[1]==x;
}
inline void update(int x){
if(x){
t[x].size=t[x].cnt;
if(t[x].son[0]) t[x].size+=t[t[x].son[0]].size;
if(t[x].son[1]) t[x].size+=t[t[x].son[1]].size;
}
}
inline int build(int l,int r,int last){
if(l>r) return 0;
int mid=(l+r)/2;
int now=++tot;
t[now]={w[mid],1,1,last,{0,0},0};
t[now].son[0]=build(l,mid-1,now);
t[now].son[1]=build(mid+1,r,now);
update(now);
return now;
}
inline void pushdown(int x){
if(x and t[x].tag){
t[t[x].son[1]].tag^=1;
t[t[x].son[0]].tag^=1;
swap(t[x].son[1],t[x].son[0]);
t[x].tag=0;
}
}
inline void rotate(int x){
int f=t[x].fa,gf=t[f].fa;
pushdown(x),pushdown(f);
int k=judgeson(x);
t[f].son[k]=t[x].son[k^1];
t[t[f].son[k]].fa=f;
t[x].son[k^1]=f;
t[f].fa=x;
t[x].fa=gf;
if(gf){
t[gf].son[t[gf].son[1]==f]=x;
}
update(f);
}
inline void splay(int x){
for(int f;(f=t[x].fa);rotate(x)){
if(t[f].fa){
if(judgeson(x)==judgeson(f)){
rotate(f);
}
else rotate(x);
}
}
root=x;
}
inline void splay(int x,int goal){
for(int f;(f=t[x].fa)!=goal;rotate(x)){
if(t[f].fa!=goal){
if(judgeson(x)==judgeson(f)){
rotate(f);
}
else{
rotate(x);
}
}
}
if(goal==0){
root=x;
}
}
inline void insert(int x){
if(!root){
tot++;
t[tot]={x,1,1,0,{0,0}};
root=tot;
return;
}
int now=root,fa=0;
while(1){
if(x==t[now].w){
t[now].cnt++;
update(now);update(fa);
splay(now);
break;
}
fa=now;
now=t[now].son[t[now].w<x];
if(!now){
tot++;
t[tot]={x,1,1,fa,{0,0}};
t[fa].son[t[fa].w<x]=tot;
update(fa);
splay(tot);
break;
}
}
}
inline int find(int x){
int now=root;
while(now){
pushdown(now);
if(x<=t[t[now].son[0]].size){
now=t[now].son[0];
}
else{
x-=t[t[now].son[0]].size+1;
if(!x) return now;
now=t[now].son[1];
}
}
return 0;
}
inline void reverse(int L,int R){
int l=L-1,r=R+1;
l=find(l),r=find(r);
splay(l,0);
splay(r,l);
int p=t[root].son[1];
p=t[p].son[0];
t[p].tag^=1;
}
inline int findnum(int rank){
int now=root;
while(now){
if(t[now].son[0] and rank<=t[t[now].son[0]].size){
now=t[now].son[0];
}
else{
int temp=t[now].cnt;
if(t[now].son[0]){
temp+=t[t[now].son[0]].size;
}
if(rank<=temp) return t[now].w;
rank-=temp;
now=t[now].son[1];
}
}
return t[now].w;
}
inline int findrank(int val){
int now=root,ans=0;
while(now){
if(val<t[now].w){
now=t[now].son[0];
}
else{
if(t[now].son[0]){
ans+=t[t[now].son[0]].size;
}
if(val==t[now].w){
splay(now);
return ans;
}
ans+=t[now].cnt;
now=t[now].son[1];
}
}
return ans;
}
inline int findpre(){
int now=t[root].son[0];
while(t[now].son[1]) now=t[now].son[1];
return now;
}
inline int findnext(){
int now=t[root].son[1];
while(t[now].son[0]) now=t[now].son[0];
return now;
}
inline int findpre(int val){
insert(val);
int ans=findpre();
free(val);
return ans;
}
inline int findnext(int val){
insert(val);
int ans=findnext();
free(val);
return ans;
}
inline int got(int id){
return t[id].w;
}
inline void free(int x){
findrank(x);
if(t[root].cnt>1){
t[root].cnt--;
update(root);
return;
}
if(!t[root].son[0] and !t[root].son[1]){
clear(root);
root=0;
return;
}
if(!t[root].son[0]){
int oroot=root;
root=t[root].son[1];
t[root].fa=0;
clear(oroot);
return;
}
else if(!t[root].son[1]){
int oroot=root;
root=t[root].son[0];
t[root].fa=0;
clear(oroot);
return;
}
int left=findpre(),oroot=root;
splay(left);
t[root].son[1]=t[oroot].son[1];
t[t[oroot].son[1]].fa=root;
clear(oroot);
update(root);
}
inline void search(int now){
pushdown(now);
if(t[now].son[0]) search(t[now].son[0]);
if(t[now].w!=inf and t[now].w!=-inf){
search_answer.push_back(t[now].w);
}
if(t[now].son[1]) search(t[now].son[1]);
}
inline void printanswer(char devide,char ending){
for(int i:search_answer){
cout<<i<<devide;
}
cout<<ending;
}
};
}
using namespace splay;
2.4 记忆提示
全局变量(2)
结构体(1,1,1,1,2)
update(int): 更新节点 $x$ 的子树大小
hint: 自己+左+右
judgeson(int)
hint: 左0 右1
rotate(int): 旋转当前点与父节点
hint:
f,gf
(下放)
judgeson(x)
x异侧孩子上移(到x同侧)改fa
fa[x]下移(到异侧)改fa
x改fa到fa[fa[x]],非空改son(fa[x]同侧取代)
更新fa[x]
2.5 常见问题
Splay TLE 了
假如你在任何一个查询操作里死循环而不输出答案,请考虑以下原因:
- 注意特判根节点,假如你的代码可能会因为 \(fa_{root}=root\) 死循环.
- 请检查有没有进行节点跳转
否则,如果你只是运行速度较慢,请检查你的 Splay 函数,未成功 Splay(或未成功旋转导致效率太低)可能是一个原因.
奇怪的 WA
因为平衡树的各种概念定义并没有一个统一的规范,不同的题目对于各种操作的定义可能是不同的,函数并不能完全做到符合题目的要求,需要进行适当改装.
比如,对于查询 \(rank\) 的操作,假如定义是 “该元素第一个出现的排名”,则需要你将函数的答案加 \(1\),假如定义是 “该元素第最后一个出现的排名”,则需要你将函数的答案加该节点的 \(size\).
3.Treap
P6136 [C++14 -O2] 平均耗时 \(11.8950\) s | [C++14] 平均耗时 \(12.9575\) s
3.1 算法思想
基本思路:Treap=Tree+Heap
特点:跑得非常快,好写,好平衡树,赞了
Tree 在这里指的就是二叉查找树,因为我们要维护二叉查找树的平衡,显然这里必须要有一个. 这也就是说,我们在 Splay 中使用的 rotate() 函数是通用的.
Heap 即为堆,因为我们重点要解决的是退化成链的问题,而二叉堆(优先队列)恰好能够自己平衡自己,所以我们尝试用二叉查找树的性质来维护二叉堆的平衡.
所以我们应该给每个节点都赋两个值,一个是该节点的真实值(用于二叉查找树),另一个是我们为了保持平衡赋的值(用于二叉堆),那么我们需要干的就是利用 rotate() 操作来实现这个二叉堆的操作.
那么我们手动赋的这个值应该是多少呢,不知道,但是可以随便给,理论证明随机给数是大概率平衡的. 我们的随机种子应该是固定的,即使可以换,但是也不应该用 srand(time(0)) 之类的函数(其实没啥影响,但就是会有点影响)
可以看出这个算法还是比较看脸吃饭的
其实 Treap 的实现还是非常简单的. 二叉堆的实现我们都知道:插入的时候先插在最外面,再通过比较进行旋转(只不过这里我们需要维护二叉查找树所以只能左右旋罢了),删除的时候就先换到最下面(怎么换都行),然后删了它再平衡就行了.
3.2 核心代码
3.2.1 更新
更新就和 Splay 的没啥区别了,在这里提供一种更简单的写法,因为子树不存在的话 \(size\) 本来就是 \(0\),加上也没啥影响,因此就不用判了.
inline void update(int x){
t[x].size=t[t[x].son[0]].size+t[t[x].son[1]].size+t[x].cnt;
}
3.2.2 旋转
同 2.2.2
这里和 Splay 最大的差异就是这里传了一个 &id,其实这里在实现的时候主要是传 \(root\) 进去,所以引用的话能比较方便地改根节点.
这里的 \(isright\) 即可以理解成右旋的 \(right\),也可以理解成左孩子与右孩子(\(isright=1\) 就是把右孩子翻上来)
inline void rotate(int &id,int isrignt){
bool k=isrignt;
int temp=t[id].son[k^1];
t[id].son[k^1]=t[temp].son[k];
t[temp].son[k]=id;
id=temp;
update(t[id].son[k]);
update(id);
}
3.2.3 新建节点
其实这个函数可以没有的... 按需写吧
inline int newnode(int val){
t[++tot]={val,rand(),1,1,{0,0}};
return tot;
}
3.2.4 插入
这里为了方便,我们把函数写成递归的形式
- 未找到就新建节点
- 如果恰好找到的话,记一个 \(cnt\).
- 否则,根据二叉查找树的性质递归搜索.
- 递归返回后切记要根据堆性质旋转(这份代码用的是小根堆),假如递归时用的是 \(son[k]\),那么旋转时则要用 \(rotate_{!k}\)
- 记得更新(在全局写)
inline void insert(int &id,int x){
if(!id){
id=newnode(x);//新建
return;
}
if(x==t[id].w) t[id].cnt++;//找到了
else{
bool k=(x>=t[id].w);
insert(t[id].son[k],x);//递归插入
if(t[id].data<t[t[id].son[k]].data){//按堆性质旋转
rotate(id,k^1);//注意这里是^1
}
}
update(id);
}
3.2.5 删除
- 未找到直接返回
- 否则,如果找到了,首先看 \(cnt\) 能不能减(记得更新),不能的话就把它换到最下面再删掉
- 删除节点的步骤是:先判断有没有孩子,没有直接删,否则将它孩子中较小的换上来(这里用的就是随机赋的那个值了,换比较小的是因为要维护小根堆性质).
- 否则就是进一步在子树里找,还是根据二叉查找树性质. 回来别忘了更新
inline void remove(int &id,int x){
if(!id) return;//未找到
if(t[id].w==x){//找到了
if(t[id].cnt>1){
t[id].cnt--;
update(id);
return;
}
if(t[id].son[0] or t[id].son[1]){
if(!t[id].son[1] or t[t[id].son[0]].data>t[t[id].son[1]].data){
rotate(id,1);//只有左儿子,或者左儿子更小
remove(t[id].son[1],x);//转下来了直接递归即可
}
else{
rotate(id,0);//否则转右儿子
remove(t[id].son[0],x);
}
update(id);
}
else{
id=0;//没有儿子
}
return;
}
(x<t[id].w)?remove(t[id].son[0],x):remove(t[id].son[1],x);//递归删除
update(id);
}
3.2.6 查询元素排名
- 没找到就返回 \(1\)(具体看怎么定义了,在这里返回 \(1\) 是因为我们递归需要).
- 找到了就返回它左子树的 \(size\) 加上 \(1\) (这里这个 \(1\) 也按需添加)
- 然后就是分情况找了,假如在左子树那就直接返回答案,在右子树还要加上该节点 \(cnt\) 和左子树的 \(size\)
inline int getrank(int id,int x){
if(!id){
return 1;//没找到
}
if(x==t[id].w){
return t[t[id].son[0]].size+1;//当前值
}
else if(x<t[id].w){
return getrank(t[id].son[0],x);//左子树
}
else{
return t[t[id].son[0]].size+t[id].cnt+getrank(t[id].son[1],x);//右子树
}
}
3.2.7 查询元素值
- 没找到返回无解
- 否则还是分类讨论,通过 \(rank\) 和左子树 \(size\) 比一下可以判断是不是在左子树,再通过判断 \(rank\) 和左子树 \(size\) 加上当前 \(cnt\) 的关系来判断是不是在当前节点,还不是就要搜右子树了. 注意搜右子树的时候记得要给 \(rank\) 减去左边这些值.
inline int getval(int id,int rank){
if(!id) return inf;//没找到
if(rank<=t[t[id].son[0]].size){
return getval(t[id].son[0],rank);//左子树
}
else if(rank<=t[t[id].son[0]].size+t[id].cnt){
return t[id].w;//当前值
}
else{
return getval(t[id].son[1],rank-t[t[id].son[0]].size-t[id].cnt);//右子树
}
}
3.2.8 查找前驱与后继
其实这个倒是没啥好说的,按二叉搜索树性质一直跳就行了,要注意的还是跳到头及时退出.
inline int getpre(int x){//前驱
int id=root,pre=0;
while(id){
if(t[id].w<x){
pre=t[id].w;
id=t[id].son[1];
}
else{
id=t[id].son[0];
}
}
return pre;
}
inline int getnext(int x){//后继
int id=root,next=0;
while(id){
if(t[id].w>x){
next=t[id].w;
id=t[id].son[0];
}
else{
id=t[id].son[1];
}
}
return next;
}
3.3 完整代码
#include<bits/stdc++.h>
using namespace std;
namespace balanced_tree{
const int N=100001,inf=114514191;
class treap{
private:
int tot;
struct tree{
int w,data,size,cnt,son[2];
}t[N];
public:
int root;
inline void update(int x){
t[x].size=t[t[x].son[0]].size+t[t[x].son[1]].size+t[x].cnt;
}
inline int newnode(int val){
t[++tot]={val,rand(),1,1,{0,0}};
return tot;
}
inline void rotate(int &id,int isrignt){
bool k=isrignt;
int temp=t[id].son[k^1];
t[id].son[k^1]=t[temp].son[k];
t[temp].son[k]=id;
id=temp;
update(t[id].son[k]);
update(id);
}
inline void insert(int &id,int x){
if(!id){
id=newnode(x);
return;
}
if(x==t[id].w) t[id].cnt++;
else{
bool k=(x>=t[id].w);
insert(t[id].son[k],x);
if(t[id].data<t[t[id].son[k]].data){
rotate(id,k^1);
}
}
update(id);
}
inline void remove(int &id,int x){
if(!id) return;
if(t[id].w==x){
if(t[id].cnt>1){
t[id].cnt--;
update(id);
return;
}
if(t[id].son[0] or t[id].son[1]){
if(!t[id].son[1] or t[t[id].son[0]].data>t[t[id].son[1]].data){
rotate(id,1);
remove(t[id].son[1],x);
}
else{
rotate(id,0);
remove(t[id].son[0],x);
}
update(id);
}
else{
id=0;
}
return;
}
(x<t[id].w)?remove(t[id].son[0],x):remove(t[id].son[1],x);
update(id);
}
inline int getrank(int id,int x){
if(!id){
return 1;
}
if(x==t[id].w){
return t[t[id].son[0]].size+1;
}
else if(x<t[id].w){
return getrank(t[id].son[0],x);
}
else{
return t[t[id].son[0]].size+t[id].cnt+getrank(t[id].son[1],x);
}
}
inline int getval(int id,int rank){
if(!id) return inf;
if(rank<=t[t[id].son[0]].size){
return getval(t[id].son[0],rank);
}
else if(rank<=t[t[id].son[0]].size+t[id].cnt){
return t[id].w;
}
else{
return getval(t[id].son[1],rank-t[t[id].son[0]].size-t[id].cnt);
}
}
inline int getpre(int x){
int id=root,pre=0;
while(id){
if(t[id].w<x){
pre=t[id].w;
id=t[id].son[1];
}
else{
id=t[id].son[0];
}
}
return pre;
}
inline int getnext(int x){
int id=root,next=0;
while(id){
if(t[id].w>x){
next=t[id].w;
id=t[id].son[0];
}
else{
id=t[id].son[1];
}
}
return next;
}
};
}
using namespace balanced_tree;
4.FHQTreap
这个东西有种暴力数据结构的美,所以很好写,但是常数大,比较推荐
4.1 算法思想
把树有序地拆开,执行完了再装上,同时用 Tree+Heap 平衡
那么首先我们就要讲的是把树有序地拆开
因为显然二叉搜索树已经有序了,我们只需要决定在哪断就行了,首先从根节点开始搜起,假如根节点小于这个值,说明整个左子树都小于这个值,那就把左子树全塞进去,然后递归右子树. 否则就递归左子树,这很无脑.
拆开之后就很方便了,假如我们要插入一个数 \(x\),那么显然需要先按 \(x\) 来把树拆开,然后在端点处直接拼一下,再直接拼上就行了(其实这里用普通二叉搜索树的方法插入元素比较快,但是这么做明显比较简单,你都用 FHQ 了还要什么速度),删除也是同理.
至于拼上也比较简单,因为两颗子树一定是有序的,所以只需要判断哪一棵子树符合我们定义的堆条件,然后粘上就行了.
显然这样做还没提到平衡树的平衡,既然这东西都叫 Treap 了,显然也是用一样的思路去维护,