Splay 学习笔记
一:初步认识
Splay 是一种平衡树,代码量不如无旋 Treap,支持的操作无旋 Treap 大都也支持。
那为什么我们要学它呢?理性娱乐 因为有一个叫 LCT 的神奇数据结构需要用到它。
二:实现
这部分所有的可能有图片的内容都可以到 oiwiki 的 Splay 条目 找到,本文的实现大部分也都是参考这篇文章的。
一些约定
int rt; //平衡树的根
int val[N]; //val[x]:表示节点 x 对应的权值
int cnt[N]; //cnt[x]:表示节点 x 对应权值的个数
int sz[N]; //sz[x] :表示节点 x 的子树大小
int fa[N]; //fa[x] :表示节点 x 的父节点
int ch[N][2];//ch[x] :表示节点 x 的子节点
int tot; //有多少节点
核心思想
Splay 通过每次操作后将访问到的节点移动至根节点来保证时间复杂度。
核心操作
Rotate 旋转操作
rotate(x)
的功能就是在保持二叉搜索树的性质的前提下,通过旋转把节点 \(x\) 向上移动。
#define Get(x) (x==ch[fa[x]][1])
void push_up(int x){sz[x]=cnt[x]+sz[ch[x][0]]+sz[ch[x][1]];}
void rotate(int x){
int y=fa[x],z=fa[y],id=Get(x);
if(z) ch[z][Get(y)]=x;
ch[y][id]=ch[x][id^1],ch[x][id^1]=y;
if(ch[y][id]) fa[ch[y][id]]=y;
fa[y]=x,fa[x]=z;
push_up(y);
push_up(x);
}
Splay 伸展操作
Splay(x)
通过三种操作将 \(x\) 节点移动到根节点处,并成为新的根节点。
void Splay(int &z,int x){
int w=fa[z];
for(int y;(y=fa[x])!=w;rotate(x))
if(fa[y]!=w)
rotate(Get(x)==Get(y) ? y : x);
z=x;
}
平衡树操作
根据权值找节点
像其他平衡树一样根据二叉搜索树的性质进行搜索即可。不要忘了将访问到节点移到根节点处!!!
这个函数有一个额外功能:如果不存在这个权值,那么它会将第一个大于/小于该权值的节点移到根节点处。
void find(int &z,int v){
int x=z,y=fa[z];
while(x && val[x]!=v)
y=x,x=(v<val[x] ? ch[x][0] : ch[x][1]);
Splay(z,x ? x : y);
}
根据排名找节点
像其他平衡树一样根据二叉搜索树的性质进行搜索即可。不要忘了将访问到节点移到根节点处!!!
void find_rak(int &z,int k){
int x=z;
while(x){
if(k<=sz[ch[x][0]])
x=ch[x][0];
else if(k<=sz[ch[x][0]]+cnt[x])
break;
else
k-=sz[ch[x][0]]+cnt[x],x=ch[x][1];
}
Splay(z,x);
}
插入操作
先找到这个权值对应的节点,如果不存在这个节点,就新开节点;否则就维护节点信息。
void insert(int v){
int x=rt,y=0;
while(x && val[x]!=v)
y=x,x=(v<val[x] ? ch[x][0] : ch[x][1]);
if(x) ++sz[x],++cnt[x];
else{
x=++tot;
sz[x]=cnt[x]=1;
val[x]=v;
fa[x]=y;
if(y) ch[y][v>val[y]]=x;
}
Splay(rt,x);
}
合并操作
在某些操作中,我们需要合并两颗 Splay,并且保证其中一颗 \(A\) 的最小权值大于另一颗 \(B\) 的最大权值。
那么我们就把 \(A\) 中权值最小的节点移到根处,并将 \(B\) 的根节点连向 \(A\) 的根,可以证明这样合并后新的树一定满足二叉搜索树的性质。
int merge(int x,int y){
if(!x || !y) return x|y;
find_rak(y,1);
ch[y][0]=x;
fa[x]=y;
push_up(y);
return y;
}
删除操作
先找到权值对应节点,然后更新节点信息,如果节点已经为空节点,那么就合并它的左右子树。
void del(int v){
find(rt,v);
if(!rt || val[rt]!=v) return;
--cnt[rt];
--sz[rt];
if(!cnt[rt]){
int ls=ch[rt][0],rs=ch[rt][1];
fa[ls]=fa[rs]=0;
rt=merge(ls,rs);
}
}
问排名
先找到权值对应节点,如果找到的节点权值大于询问权值,那么要把当前点的个数也加上(因为不一定存在这个权值)。
int getrank(int v){
find(rt,v);
return sz[ch[rt][0]]+(val[rt]<v ? cnt[rt] : 0) +1;
}
问前驱
先找到权值对应节点,如果找到的节点权值小于询问权值,那么答案就是这个节点的权值;否则就一直跳右儿子。
int getpre(int v){
find(rt,v);
if(rt && val[rt]<v) return val[rt];
int x=ch[rt][0];
if(!x) return -1;
while(ch[x][1]) x=ch[x][1];
Splay(rt,x);
return val[x];
}
问后缀
与问前缀类似。
int getsuf(int v){
find(rt,v);
if(rt && val[rt]>v) return val[rt];
int x=ch[rt][1];
if(!x) return -1;
while(ch[x][0]) x=ch[x][0];
Splay(rt,x);
return val[x];
}