平衡树概述
维护树的中序遍历不变
非旋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类似