文章目录
Splay
参考的博客
https://www.luogu.com.cn/blog/pks-LOVING/more-senior-data-structure-te-bie-qian-di-qian-tan-splay
https://oi-wiki.org/ds/splay/(一些图片也来自于此)
介绍
Splay相对来说可以我维护一些其他平衡树无法维护的东西,但其实大部分FHQ-TREAP也可以,如一些区间操作。但最重要的还是为了LCT要不是为了它,我是真不想学Splay。但因为需要旋转,所以均摊复杂度会比FHQ-TREAP要慢一些,常数比较大。但是理论复杂度较好,不会被卡。因为带旋的话会比较难,所以篇幅会比较长。
时空复杂度
因为Splay是一颗平衡树,所以他的空间也与其他的平衡树一样,为 O ( N ) O(N) O(N) ,而Splay的平均复杂度为均摊 O ( l o g n ) O(logn) O(logn) ,注意是均摊,不是像红黑树与AVL树那样的严格 O ( l o g n ) O(logn) O(logn) ,但是要卡Splay还是非常难的。
思路:
旋转
首先,Splay最重要的就就是旋转操作,这也是它与FHQ_TREAP所不同的地方。旋转,顾名思义,就是将某个节点与其的父亲进行旋转操作。如图所示:
先讲右旋,是以2为点,1为父节点,旋转2,1两个节点。首先,因为要维护BIT的性质,所以直接选的话肯定会出现一个点有是三个子节点的情况,显然不能这样做。
为了维护这个性质,如果2有右子树,先将1的左子树设为2的右子树,再将2的右子树的父节点设为1。再讲2的右子树设为1。2的父亲节点设为1原来的父亲节点,1的父亲节点设为2,1原来的父亲节点的儿子设为2。
因为2的右子树也是1的左子树,所以直接连在1上面是正确的。而因为2的右子树与1的右子树都是都是大于2的节点,所以都放在2的右子树是正确的。
而左旋就正好与右旋相反。如图,是以1为点,与2旋转。首先将2的右子树设为1的左子树。1的左子树设为1,其他的节点关系也相应改变就可以了。而其实所有的旋转操作的本质就是讲一边的树高加一,一边的树高减一。
void rotate(int rt){//旋转当前节点
int x=t[rt].fa,y=t[x].fa,chx=get(rt),chy=get(x);//x为父亲节点,y为爷爷节点,chx表示当前节点的父子关系
t[x].ch[chx]=t[rt].ch[chx^1];//先将rt的父节点的儿子更新了
if(t[rt].ch[chx ^ 1])t[t[rt].ch[chx ^ 1]].fa=x;
t[rt].ch[chx^1]=x;
t[x].fa=rt;
t[rt].fa=y;
if(y)t[y].ch[chy]=rt;
update(x);//更新几个节点的值,因为他们的结构改变了
update(rt);
//不用更新爷爷节点 y ,因为他的子树总和还是一样的
return ;
}
Splay操作
在Splay中1,为了保证树的平衡性,每次询问或插入一个点的时候都需要将其提为根节点。而在其不断旋转的过程中,会出现许多不同的情况,我们对于每一种情况进行具体分析(在之后将需要旋转的节点设为x,x的父节点设为p):
-
若p是根节点,直接旋转两个节点就可以了。此时整棵树的大体结构并不会发生变化(但其实两边的子树深度一边+1,一边-1,需要靠一些其他的操作维持平衡
还是FHQ_TREAP好,随机就行)。 -
若p不是根节点,即p还有父亲,且x,p,fa[p] ,三个节点在一条直线上(或是说x与p的父子关系相同,都为左儿子或都为右儿子),就先旋转p,之后在旋转x节点。(许多题解这里都没有讲为什么,自己想了许久,这里讲清楚希望能让其他人更好的理解)看下面第二张图可知,如果这样直接旋转的话,x,p,g三点的关系就乱掉了,从xpg变成了xgp,三点也不再同一条直线上了。而如果我们先旋转p,再旋转x的除了x,其他所有点的相对关系还没有改变。(为什么不能改变还不懂,所有操作搞懂以后再回来补)。
update :破案了,就是没有区别。其实不管怎么选都是x在最上面,gp在x的下面,且g在p下面或p在g下面。即两边子树的树高还是一定的。结果是一样的,只是这样不一样的旋转方法
或许可以让这颗树更平衡一点吧。因为一直从一边旋转是容易让某一颗子树的高度过低,也容易被特殊数据卡掉。所以说随机旋转是不是也可以?话说好像是有TREAPLAY。(等我几种都试试在回来打结果)
-
若p不是根节点,但x,p,fa[p],不在一条直线上的话,可以直接将x旋转两次。
void splay(int rt,int to){//splay操作,即将当前节点旋转到to的子节点
while(t[rt].fa!=to&&rt){//还有父节点,即还没旋转到根节点
if(t[t[rt].fa].fa!=to)rotate(get(rt) == get(t[rt].fa) ? t[rt].fa:rt);
else rotate(rt);
//还有爷爷节点,需要特判一下,防止被卡
}
if(to==0)root=rt;//更新根节点
}
插入
插入操作是SPLAY中一种非常恶心的操作,很麻烦,一下设要加入的值为 k。(好吧,其实也还好)
-
先比较当前节点的值与 k 的大小差别,如果在根据BIT的性质进入左右儿子。
-
若果当前节点的值等于 k 或是当前节点为空节点,加将数量加一(Splay是将所有权值相等的节点放在同一个点中的,即每个点还会记录一个出现次数)或是插入新节点 。
-
对进行修改的节点进行一次 SPLAY操作(即将他提到根节点)
void init(int rt,int f,int k){//插入节点
if(!rt){//如果当前节点为空,就添加一个新节点
t[++tot].value=k;
t[tot].fa=f;
t[tot].cnt++;//计数器
if(f)t[f].ch[t[f].value < k]=tot;//记得要一起更新父节点
splay(tot,0);//所有的update在splay操作中更新
return ;
}
if(t[rt].value==k){
t[rt].cnt++;
splay(rt,0);//千万不要忘记splay,所有return操作前都要splay
return ;
}
init(t[rt].ch[t[rt].value < k ],rt,k);//根据BIT的性质进入左右儿子
return ;
}
查询 k 的排名
后面的操作就都差不多了,都是在树上根据BIT性质跑,并且在最后进行SPLAY操作就可以了(下面的排名将1看为第一名)。
-
如果当前节点比 k 小,将贡献加上左子树的size再+1,然后进入右儿子。
-
如果当前节点比 k 大,直接进入左儿子。
-
如果当前节点的值等于 k ,返回当前贡献+1,并对当前节点进行一次SPLAY操作(请一定要记住,所有操作后都要进行SPLAY操作)。
int rank_k(int rt,int k,int sum){
if(t[rt].value==k){
sum+=t[t[rt].ch[0]].size;
splay(rt,0);//进行splay操作,不然会炸
return sum;
}
if(t[rt].value<k)return rank_k(t[rt].ch[1],k,sum+t[t[rt].ch[0]].size+t[rt].cnt);
//左子树都是比 k 要小的数,直接加上就可以了
else return rank_k(t[rt].ch[0],k,sum);
}
查询排名为 k 的数
还是根据BIT的性质,在树上查找就可以了。
-
如果当前节点的左儿子的size大于 k ,进入左儿子。
-
不然将k减去(size+cnt),如果 k 小于等于 0 的话,就返回当前节点的值,不然进入右儿子。
-
记得进行 SPLAY操作。
int find_k(int rt,int k){//查询排名为 k 的数
if(t[t[rt].ch[0]].size>=k)return find_k(t[rt].ch[0],k);//记得是大于等于,因为等于也要往左子树走
else if(k-t[t[rt].ch[0]].size-t[rt].cnt<=0)return t[rt].value;//如果小于等于0说明是当前节点
return find_k(t[rt].ch[1],k-t[t[rt].ch[0]].size-t[rt].cnt);
}
查询 x 的前驱
前驱定义为小于 x 的最大的树。所以我们可以进行一些转换,将x提为根,在查询就可以了。
-
先在BIT上进行查找,找到x的位置,对其进行SPLAY操作,将其提到根节点。
-
在现在的树上找 x 的左子树中最右的一个节点。
-
返回最右的节点并进行SPLAY操作
int pre(int rt,int k){//求前驱
if(!rt)return 0;
if(t[rt].value==k){//如果当前节点是要找的节点
splay(rt,0);//先将这个节点提到根节点
int cur=t[rt].ch[0];//cur表示的即为当前点的前驱
if(!cur)return cur;
while(t[cur].ch[1])cur=t[cur].ch[1];//找左子树中最大的节点
return cur;
}
return pre(t[rt].ch[t[rt].value < k],k);
}
查询 x 的后继
后继定义为大于 x 的最小的树。因为定义与前驱恰好相反,所以操作也反过来就可以了。
-
先在BIT上进行查找,找到x的位置,对其进行SPLAY操作,将其提到根节点。
-
在现在的树上找 x 的右子树中最左的一个节点。
-
返回最左的节点并进行SPLAY操作
int suf(int rt,int k){
if(!rt)return 0;
if(t[rt].value==k){//如果当前节点是要找的节点
splay(rt,0);
int cur=t[rt].ch[1];//cur表示的即为当前点的后继
if(!cur)return cur;
while(t[cur].ch[0])cur=t[cur].ch[0];//找右子树中最小的节点
return cur;
}
return suf(t[rt].ch[t[rt].value < k],k);
}
删除 x
Splay因为每个点还会存储出现次数,但又是平衡树,所以只在FHQ_TREAP上稍改一点就可以了。
-
先在BIT上找到 x 对其进行SPLAY操作并提到根节点。
-
如果当前节点的cnt(出现次数)大于 1 , 直接减一在退出就可以了。
-
如果cnt为1,先找到 x 的前驱与后继,分别为 a b ,现将a提到根节点, b 提到a的子节点。
-
将 b 的左儿子设为 0
关于这样做法的正确性,在下面给出图解证明:
首先,将 x 提到根节点,会有左右两颗子树,他的前驱 a 为 5 ,后继值为 7 。 而如果先将a,b提到 x 的儿子节点的话,a会没有右子树,b会没有左子树。而再将a与b旋转的话,x 会成为 a 的右儿子 ,而如果再将 b 与 x 旋转的话,x 会成为 b 唯一的一个左儿子。所以直接将 b 的左儿子设为 0 就可以了。
void cut(int rt,int f,int k){//删除 k
if(t[rt].value == k){//已经找到了要删除的节点 k 了
splay(rt,0);//将 k 提到根节点上去
if(t[rt].cnt>1){
t[rt].cnt--;//数量大于1的话直接减一就可以了,不用改变树的结构
return ;
}
else{
int x=pre(rt,k);//找到前驱
int y=suf(rt,k);//找到后继
splay(x,0);
splay(y,x);
t[y].ch[0]=0;//删除这个节点
return ;
}
}
cut(t[rt].ch[t[rt].value < k],rt,k);
return ;
}
特殊数据的卡法
根据SPLAY的插入可知,我们只要一次输入1~n ,就可以是Splay的树高变成 n ,而后面只要一次询问 n,1,n,1…这样循环就会将Splay卡成 $ O(n^2) $ ,这也就说明了上文特判的左右,因为先旋转父亲是会破坏链的结构的,这样就使得这样的循环询问可能降低树高,而不是一直为 n 。其实个人觉得这一操作改成随机化的话可能结果也差不多。
具体代码
定义的结构体:
struct node{
int lc,rc;//左右儿子
int cnt;//当前值的树出现了几次
int size//左右子树的cnt之和,去其他的size有区别
int fa;//父亲节点
int value;//当前节点的大小
}t[N];
完整代码:
#include<bits/stdc++.h>
using namespace std;
const int N=100011;
struct node{
int ch[2];//左右儿子 0为左儿子 1为右儿子
int cnt;//当前值的树出现了几次
int size;//当前点的cnt之和,去其他的size有区别
int fa;//父亲节点
int value;//当前节点的权值
}t[N<<1];
int n,tot;
int root;//根节点
struct Splay_tree{
void update(int rt){//更新当前节点的值
t[rt].size=t[t[rt].ch[0]].size+t[t[rt].ch[1]].size+t[rt].cnt;
return ;
}
int get(int rt){//返回当前节点的父子关系,即当前节点是其父节点的左儿子还是右儿子
return t[t[rt].fa].ch[1] == rt ;//若当前节点为右儿子,就会返回 1 ,不然返回 0
}
void clear(int rt){//清空当前节点
t[t[rt].fa].ch[get(rt)]=0;//更新父亲节点
t[rt].ch[0]=t[rt].ch[1]=t[rt].fa=t[rt].size=t[rt].cnt=0;//更新自己的值
t[t[rt].ch[0]].fa=t[t[rt].ch[1]].fa=0;//将他的儿子也要更新
return ;
}
void rotate(int rt){//旋转当前节点
int x=t[rt].fa,y=t[x].fa,chx=get(rt),chy=get(x);//x为父亲节点,y为爷爷节点,chx表示当前节点的父子关系
t[x].ch[chx]=t[rt].ch[chx^1];//先将rt的父节点的儿子更新了
if(t[rt].ch[chx ^ 1])t[t[rt].ch[chx ^ 1]].fa=x;
t[rt].ch[chx^1]=x;
t[x].fa=rt;
t[rt].fa=y;
if(y)t[y].ch[chy]=rt;
update(x);//更新几个节点的值,因为他们的结构改变了
update(rt);
//不用更新爷爷节点 y ,因为他的子树总和还是一样的
return ;
}
void splay(int rt,int to){//splay操作,即将当前节点旋转到to的子节点
while(t[rt].fa!=to&&rt){//还有父节点,即还没旋转到根节点
if(t[t[rt].fa].fa!=to)rotate(get(rt) == get(t[rt].fa) ? t[rt].fa:rt);
else rotate(rt);
//还有爷爷节点,需要特判一下,防止被卡
}
if(to==0)root=rt;//更新根节点
}
void init(int rt,int f,int k){//插入节点
if(!rt){//如果当前节点为空,就添加一个新节点
t[++tot].value=k;
t[tot].fa=f;
t[tot].cnt++;//计数器
if(f)t[f].ch[t[f].value < k]=tot;//记得要一起更新父节点
splay(tot,0);
return ;
}
if(t[rt].value==k){
t[rt].cnt++;
splay(rt,0);//千万不要忘记splay,所有return操作前都要splay
return ;
}
init(t[rt].ch[t[rt].value < k ],rt,k);//根据BIT的性质进入左右儿子
return ;
}
int pre(int rt,int k){//求前驱
if(!rt)return 0;
if(t[rt].value==k){//如果当前节点是要找的节点
splay(rt,0);
int cur=t[rt].ch[0];//cur表示的即为当前点的前驱
if(!cur)return cur;
while(t[cur].ch[1])cur=t[cur].ch[1];//找左子树中最大的节点
//splay(cur,0);
return cur;
}
return pre(t[rt].ch[t[rt].value < k],k);
}
int suf(int rt,int k){
if(!rt)return 0;
if(t[rt].value==k){//如果当前节点是要找的节点
splay(rt,0);
int cur=t[rt].ch[1];//cur表示的即为当前点的后继
if(!cur)return cur;
while(t[cur].ch[0])cur=t[cur].ch[0];//找右子树中最小的节点
return cur;
}
return suf(t[rt].ch[t[rt].value < k],k);
}
void cut(int rt,int f,int k){//删除 k
if(t[rt].value == k){//已经找到了要删除的节点 k 了
splay(rt,0);//将 k 提到根节点上去
if(t[rt].cnt>1){
t[rt].cnt--;//数量大于1的话直接减一就可以了,不用改变树的结构
return ;
}
else{
int x=pre(rt,k);
int y=suf(rt,k);
splay(x,0);
splay(y,x);
t[y].ch[0]=0;
return ;
}
}
cut(t[rt].ch[t[rt].value < k],rt,k);
return ;
}
int rank_k(int rt,int k,int sum){
if(t[rt].value==k){
sum+=t[t[rt].ch[0]].size;
splay(rt,0);
return sum;
}
if(t[rt].value<k)return rank_k(t[rt].ch[1],k,sum+t[t[rt].ch[0]].size+t[rt].cnt);
//左子树都是比 k 要小的数,直接加上就可以了
else return rank_k(t[rt].ch[0],k,sum);
}
int find_k(int rt,int k){//查询排名为 k 的数
if(t[t[rt].ch[0]].size>=k)return find_k(t[rt].ch[0],k);
else if(k-t[t[rt].ch[0]].size-t[rt].cnt<=0)return t[rt].value;
return find_k(t[rt].ch[1],k-t[t[rt].ch[0]].size-t[rt].cnt);
}
}ST;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
ST.init(root,0,INT_MAX);
ST.init(root,0,-1000000000);
//保证有前驱后继,不然查询的时候会出错
for(int i=1;i<=n;i++){
int opt,k;
cin>>opt>>k;
if(opt==1){//插入 k
ST.init(root,0,k);
}
if(opt==2){//删除 k
ST.cut(root,0,k);
}
if(opt==3){//查询 k 的排名
cout<<ST.rank_k(root,k,0)<<endl;
}
if(opt==4){//查询排名为 k 的数
cout<<ST.find_k(root,k+1)<<endl;
}
if(opt==5){//找前驱
ST.init(root,0,k);
cout<<t[ST.pre(root,k)].value<<endl;
ST.cut(root,0,k);
}
if(opt==6){//找后继
ST.init(root,0,k);
cout<<t[ST.suf(root,k)].value<<endl;
ST.cut(root,0,k);
}
}
}