平衡树初探
Splay
注意事项
0.rotate()左旋右旋。找点规律可以写一起;
splay()利用rotate()把一个节点转到目标点的儿子处;
find()找到一个节点并把它转到根,没有这个节点那就可能是把他的前驱或者后继转上来;
nxt()利用find()找前驱、后继,把x转上来之后,暴力跳子树就可以了;
insert()向下查找时要同时记录父节点;
pop()利用nxt()找到前驱后继,然后把前驱旋到根,后继旋到前驱儿子处,这样x就在后继的右儿子处且x没有儿子;
kth()注意是<=不是<;
rnk()直接find()然后取\(sum[ch[rt][0]]+1\)即可;
区间修改问题可以把l-1旋到根,r+1旋到根的儿子,那么区间\([l,r]\)就在节点r+1的左子树,打个reverse标记即可。等询问到这个节点时,先下传标记。修改后update,进入子树前pushdown,能splay就splay.
1.更新节点可以直接splay(),不需要做两次update(),因为rotate()过程中会update()。(除了删除一个节点时,即cnt=0时,因为这个时候这个节点已经从树上删去,不可能从他开始splay)。
2.操作完能splay()就尽量splay()。
3.开局insert()inf和-inf两个哨兵节点,以防要查找的值比树上值都小/大。同时要注意rnk()和kth()要相应做微调。
4.find()的while做完后p不能为0(因为最后要splay(),如果是0做splay()会把rt也置为0),而insert里的可以为0(当p为0时会新增一个节点,p就不为0了)。
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
register int x=0,f=1;register char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
if(f)return x;return -x;
}
const int N=1e5+10,inf=0x7fffffff;
namespace Splay{
int tot,rt,val[N],cnt[N],sum[N],fa[N],ch[N][2];
inline int getdir(int nw){return nw==ch[fa[nw]][1];}
inline void update(int nw){sum[nw]=cnt[nw]+sum[ch[nw][0]]+sum[ch[nw][1]];}
inline void rotate(int nw){
int f=fa[nw],as=fa[f],k=getdir(nw);
ch[f][k]=ch[nw][k^1],fa[ch[nw][k^1]]=f;
ch[nw][k^1]=f,fa[f]=nw;
fa[nw]=as;if(as)ch[as][ch[as][1]==f]=nw;
update(f),update(nw);
}
inline void splay(int nw,int goal=0){
for(int f;(f=fa[nw])^goal;rotate(nw))
if(fa[f]^goal)rotate(getdir(f)==getdir(nw)?f:nw);
if(!goal)rt=nw;
}
inline void find(int x){
int p=rt;if(!p)return;
while(val[p]^x&&ch[p][val[p]<x])p=ch[p][val[p]<x];
splay(p);
}
inline int nxt(int x,int type){
find(x);int p=rt;
if((val[p]<x&&!type)||(val[p]>x&&type))return p;
p=ch[p][type];type^=1;
while(ch[p][type])p=ch[p][type];
return p;
}
inline void insert(int x){
int p=rt,f=0;
while(val[p]^x&&p)p=ch[f=p][val[p]<x];
if(p){++cnt[p],splay(p);return;}
p=++tot;val[p]=x,cnt[p]=sum[p]=1,fa[p]=f,ch[p][0]=ch[p][1]=0;
if(f)ch[f][val[f]<x]=p;
splay(p);
}
inline void pop(int x){
int l=nxt(x,0),r=nxt(x,1);
splay(l),splay(r,l);
int p=ch[r][0];
if(cnt[p]>1){--cnt[p],splay(p);return;}
ch[r][0]=0;update(r),update(l);
}
inline int rnk(int x){
find(x);return sum[ch[rt][0]]+1;
}
inline int kth(int k){
int p=rt;
while(1){
if(k<=sum[ch[p][0]])p=ch[p][0];
else{
k-=sum[ch[p][0]];
if(k<=cnt[p])return val[p];
else k-=cnt[p],p=ch[p][1];
}
}
}
}
using namespace Splay;
int main(){
insert(inf),insert(-inf);
int n=read();
while(n--){
switch(read()){
case 1:{insert(read());break;}
case 2:{pop(read());break;}
case 3:{printf("%d\n",rnk(read())-1);break;}
case 4:{printf("%d\n",kth(read()+1));break;}
case 5:{printf("%d\n",val[nxt(read(),0)]);break;}
case 6:{printf("%d\n",val[nxt(read(),1)]);break;}
}
}
return 0;
}
fhq_treap
注意事项
0.Treap是随机附加域满足堆(例如小根堆)性质的二叉搜索树。
split有按权值分裂(权值小于等于k的在左树)和按排名分裂(就是前k大在左树),按权值用的比较多。讨论一下当前节点的情况看分到左树还是右树然后递归就可以了;
merge假设x的权值是小于y的,那么其左右的相对位置已经符合BST性质,只需考虑满足堆性质,讨论一下x和y的key值大小决定谁做根(小根堆就key小的做根),然后递归处理;
insert因为treap中元素是可重的,所以直接按权值x把treap分裂,然后加入一个新点再merge起来;
pop先按权值x分裂,再把左树按权值x-1分裂,这样得到的新的右树的点权值都是x。把新的右树根的两个儿子merge起来,就相当于把根pop掉了,然后再把三堆merge成一个treap;
rnk按权值x-1分裂,答案就是左树大小+1;
kth和splay一样实现就好了。刚开始想着用按排名分裂搞搞,但常数反而还不如暴力跳优秀;
pre按权值x-1分裂,再kth查询左树最大的就可以了(排名为左树大小的那个);
suf按权值x分裂,再kth查询右树最小的就可以了(排名为1的)。
1.任何操作做完都要merge回来,保证时刻只有一个treap。
2.不需要建哨兵节点。splay要建的原因是pop的时候要访问前驱和后继。
3.还是一样的,修改后update,进入子树前pushdown,修改完千万别忘update!!!
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
register int x=0,f=1;register char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
if(f)return x;return -x;
}
const int N=1e5+10;
int n;
namespace fhq_treap{
int val[N],key[N],ch[N][2],siz[N],cnt,rt,x,y,z;
inline int New(int k){val[++cnt]=k,siz[cnt]=1,key[cnt]=rand();return cnt;}
inline void update(int p){siz[p]=siz[ch[p][0]]+siz[ch[p][1]]+1;}
inline void split(int nw,int v,int &x,int &y){
if(!nw){x=y=0;return;}
if(val[nw]<=v)x=nw,split(ch[nw][1],v,ch[x][1],y),update(x);
else y=nw,split(ch[nw][0],v,x,ch[y][0]),update(y);
}
inline int merge(int x,int y){
if(!x||!y)return x+y;
if(key[x]<=key[y]){ch[x][1]=merge(ch[x][1],y),update(x);return x;}
else{ch[y][0]=merge(x,ch[y][0]),update(y);return y;}
}
inline void insert(int k){
split(rt,k,x,y);
rt=merge(merge(x,New(k)),y);
}
inline void pop(int k){
split(rt,k,x,y),split(x,k-1,x,z);
z=merge(ch[z][0],ch[z][1]);rt=merge(merge(x,z),y);
}
inline int rnk(int k){
split(rt,k-1,x,y);int ans=siz[x]+1;
merge(x,y);return ans;
}
inline int kth(int k,int p=rt){
while(1){
if(k<=siz[ch[p][0]])p=ch[p][0];
else if(k<=siz[p]-siz[ch[p][1]])return val[p];
else k-=siz[p]-siz[ch[p][1]],p=ch[p][1];
}
}
inline int pre(int k){
split(rt,k-1,x,y);int ans=kth(siz[x],x);
merge(x,y);
return ans;
}
inline int suf(int k){
split(rt,k,x,y);int ans=kth(1,y);
merge(x,y);
return ans;
}
}
using namespace fhq_treap;
int main(){
srand(time(0));
int n=read();
for(int t=1;t<=n;++t){
switch(read()){
case 1:{insert(read());break;}
case 2:{pop(read());break;}
case 3:{printf("%d\n",rnk(read()));break;}
case 4:{printf("%d\n",kth(read()));break;}
case 5:{printf("%d\n",pre(read()));break;}
case 6:{printf("%d\n",suf(read()));break;}
}
}
return 0;
}
例题
Luogu3369 【模板】普通平衡树
Link
板子题。
Luogu3391 【模板】文艺平衡树
Link
一旦进入子树就pushdown。
注意这两道题splay都需要建立哨兵节点。