平衡树(fhq-treap,splay)
平衡树概述
维护树的中序遍历不变
非旋Treap(fhq-treap)
每个节点有自己的参数 \(val\) 和随机值 \(ran\),fhq-treap在维护中序遍历的基础上,维护随机值 \(ran\) 的大根堆。因为 \(ran\) 随机,treap的期望复杂度为\(O(n \log n)\)
定义
struct treap{
int lson,rson,val,ran,cnt/*以当前节点为根的子树大小*/;
}tr[N];
int tot,root;//节点数和当前根
void update(int x){tr[x].cnt=tr[tr[x].lson].cnt+tr[tr[x].rson].cnt+1;}
merge
合并以\(m1\),\(m2\)为根的两颗子树(维护\(ran\)的大根堆)
int merge(int m1,int m2){
if(!m1||!m2) return m1+m2;
if(tr[m1].ran>=tr[m2].ran){
tr[m1].rson=merge(tr[m1].rson,m2);
update(m1); return m1;
}else{
tr[m2].lson=merge(m1,tr[m2].lson);
update(m2); return m2;
}
}
split
把树断开
将小于等于\(num\)的数放进以 \(l\) 为根的子树,大于\(num\)的数放进以 \(r\) 为根的子树(维护中序遍历不变)
void split(int x,int num,int &l,int &r){
if(x==0){ l=0; r=0; return ;}
if(tr[x].val<=num){
l=x;
split(tr[x].rson,num,tr[x].rson,r);
}else{
r=x;
split(tr[x].lson,num,l,tr[x].lson);
}
update(x);
return ;
}
其它操作均在merge和split的基础上
void insert(int x){//新建节点
tr[++tot].val=x; tr[tot].ran=rand();
tr[tot].cnt=1; tr[tot].lson=0; tr[tot].rson=0;
int l=0,r=0;
split(root,x-1,l,r);
root=merge(merge(l,tot),r);
}
void delet(int x){//删除大小为x的节点
int l=0,r=0,pos=0;
split(root,x,l,r); split(l,x-1,l,pos);
root=merge(merge(l,merge(tr[pos].lson,tr[pos].rson)),r);
}
int find_rank(int num){//查询x数的排名
int l=0,r=0; split(root,num-1,l,r); int ret=tr[l].cnt+1;
root=merge(l,r); return ret;
}
int find_num(int rank){//查询排名为x的数
int pos=root;
while(pos){
if(tr[pos].cnt==1) return tr[pos].val;
if(tr[tr[pos].lson].cnt>=rank) pos=tr[pos].lson;
else{
rank-=tr[tr[pos].lson].cnt;
if(rank==1) return tr[pos].val;
else rank--,pos=tr[pos].rson;
}
}
}
int find_pre(int x){//查询前驱(小于x,且最大的数)
int l=0,r=0; split(root,x-1,l,r); int pos=l;
while(tr[pos].rson) pos=tr[pos].rson;
root=merge(l,r); return tr[pos].val;
}
int find_suf(int x){//查询后继(大于x,且最小的数)
int l=0,r=0; split(root,x,l,r); int pos=r;
while(tr[pos].lson) pos=tr[pos].lson;
root=merge(l,r); return tr[pos].val;
}
例题
- (I命令)输入\(k\),若\(k>= min\_salary\), 则插入\(k\)
- (A命令)将每个数加上\(k\)
- (S命令)将每个数减去\(k\),并删除所有\(<min\)的数
- (F命令)查询第\(k\)大的数
A和S命令显然不会改变平衡树中各数的相对位置,但显然不能一个一个改
记一个变化量\(delta\),每次加减直接加减\(delta\)即可。
为了维护各数的相对大小,在插入时应该插入\(val=k-delta\)。
查询时,\(k+\Delta <min\_salary\)的所有\(k\)被删除(\(\Delta\)为区间内变化量),即\(k-delta_{pre}+delta_{now}<min\_salary\),即\(val<min\_salary-delta\)的\(val\)会被删掉
Splay
优点:可以指定哪一个点当根
基本操作:rotate和splay
rotate
在不破坏平衡树结构(中序遍历不变的情况下),将\(x\)向上旋转一次
其中的一种情况:
令\(id(x)\)表示\(x\)是其父亲的哪个儿子
inline bool id(int x){return tr[tr[x].fa].son[1]==x;}
规律如下:
- 自己的\(id(x)\ xor\ 1\)儿子变成父亲的\(id(x)\)儿子(自己的另外一边儿子替换自己的位置)
- 自己接上祖父
- 原来的父亲变成自己的\(id(x)\ xor\ 1\)儿子(即之前失去的儿子)
inline void connect(int x,int y,int pos){tr[x].fa=y;tr[y].son[pos]=x;}
inline void rotate(int x){
int f=tr[x].fa,gf=tr[f].fa;
int id1=id(f),id2=id(x);
connect(tr[x].son[id2^1],f,id2);
connect(f,x,id2^1);
connect(x,gf,id1);
update(f);update(x);
}
splay
一条链的情况会大大影响时间复杂度
我们需要通过这个操作减小链的长度
对于三点共线的情况,连续旋转是无效的
这时我们需要先旋转它的父亲,再旋转自己
否则就连续旋转两次自己
inline void splay(int x,int to){
while(tr[x].fa!=to){
if(tr[tr[x].fa].fa!=to){
if(id(x)==id(tr[x].fa))rotate(tr[x].fa);
else rotate(x);
}
rotate(x);
}
if(to==0)rt=x;
}
\(splay(x,y)\)的含义为:让\(x\)成为\(y\)的一个儿子,当\(y=0\)时,即让\(x\)成为根
没事就splay一下是好习惯,而且splay操作还有\(update\)的效果
其他操作都在这两个的基础上,但是比fhq-treap要复杂一些
\(find(x)\)
找到\(x\)所在节点,若不存在则返回离它最近的\(>\)它或\(<\)它的节点
inline int find(int x){
int pos=rt;
while(tr[pos].val!=x&&tr[pos].son[tr[pos].val<x])pos=tr[pos].son[tr[pos].val<x];
splay(pos,0);return rt;
}
\(insert(x)\)
插入一个值为\(x\)的节点
已有则直接插入,没有则找到应该插入的位置
inline void insert(int x){
int pos=rt,fa=0;
while(tr[pos].val!=x&&pos)fa=pos,pos=tr[pos].son[tr[pos].val<x];
if(pos)++tr[pos].cnt;
else{
pos=++cnt;tr[cnt].cnt=tr[cnt].size=1;tr[cnt].val=x;tr[cnt].son[0]=tr[cnt].son[1]=0;
connect(cnt,fa,tr[fa].val<x);
}
splay(pos,0);
}
\(pre(x)\)
找到\(x\)的前驱所在的节点
inline int pre(int x){
int pos=find(x);
if(tr[pos].val<x)return pos;
pos=tr[pos].son[0];
while(pos&&tr[pos].son[1])pos=tr[pos].son[1];
splay(pos,0);
return rt;
}
先找,如果找到的是小于自己的,那么就一定是小于自己的最大的,直接返回。
否则先变成小于自己的,再一直向右找到最大的
\(suf(x)\)
找到\(x\)的后继所在的节点
inline int suf(int x){
int pos=find(x);
if(tr[pos].val>x)return pos;
pos=tr[pos].son[1];
while(pos&&tr[pos].son[0])pos=tr[pos].son[0];
splay(pos,0);
return rt;
}
原理与\(pre\)类似
\(delete(x)\)
删除一个值为\(x\)的节点
inline void delet(int x){
int u=pre(x),v=suf(x);
splay(u,0);splay(v,u);
int cur=tr[v].son[0];
if(tr[cur].cnt>1){--tr[cur].cnt;splay(cur,0);}
else{tr[cur].size=tr[cur].cnt=0;tr[v].son[0]=0;update(v);update(u);}
}
先找到比自己的前驱和后继
然后以前驱为根,把后继接到前驱的儿子上(一定是右儿子),那么自己就一定在后继和前驱中间,即后继的左儿子
如果个数\(>1\),那么直接减个数即可
否则把这一条边断掉
查询排名和排名为\(x\)的数
inline int query_by_rank(int rank){
int pos=rt;
while(rank){
if(rank<=tr[tr[pos].son[0]].size)pos=tr[pos].son[0];
else if(rank>tr[tr[pos].son[0]].size+tr[pos].cnt)rank-=tr[tr[pos].son[0]].size+tr[pos].cnt,pos=tr[pos].son[1];
else{splay(pos,0);return tr[pos].val;}
}
}
inline int query_rank(int val){
int pos=pre(val);
return tr[pos].cnt+tr[tr[pos].son[0]].size;
}
根fhq-treap类似