Splay学习笔记
写在前面
蒟蒻现在才开始学splay[我真的好菜呀]
这是一篇有关splay入门学习的笔记qwq
就让splay开始吧?
首先要学习splay之前,要先知道splay是个什么玩意儿……
我们都知道二叉查找树的性质:左小右大 所以我们遍历二叉查找树的中序遍历是一个从小到大的序列qwq
二叉查找树的操作的时间复杂度是O(h)的
但是当出题人非常不良心[误]数据比较大的时候 直接使用二叉查找树显然会T……
因为当你的树被建成这样的时候,就gg了……
所以我们要引入平衡树的概念……
其实所谓的平衡树就是更加“平衡”的二叉搜索树
那么我们要怎么让这棵树更加“平衡”呢?
旋转这棵树上的节点!
rotate操作
rotate操作是splay的灵魂啊
我们考虑把一个点旋转到它的父亲会发生什么……
如果这个节点是它父亲的右儿子,那么把它旋到父亲之后,它的父亲就没有右儿子了
那么怎么办呢……
再想想,这个时候我们把它旋转到了父亲,原本的父亲就是它的左儿子啦[因为它比父亲大]
这个时候它的左儿子要往哪放呢……
这时候我们想起来父亲的右儿子没了……
对!就把它放到父亲的右儿子
这棵树仍然保持了二叉搜索树的性质
因为本来儿子节点的左子树小于儿子节点,但是大于父亲节点,所以把它塞到柚子树右子树是没有错的
然后这棵二叉搜索树的性质就是没错的啦x
图大概这样……假设我们要把A旋到B
第二幅图是旋转之后的样子 左旋和右旋同理
接下来稍微看一下代码吧……
void rotate(int x) { int fa1=fa[x]; int fa2=fa[fa1]; int K=Get(x); int K1=Get(fa1); Son[fa1][K]=Son[x][K^1]; fa[Son[fa1][K]]=fa1; Son[x][K^1]=fa1; fa[fa1]=x; fa[x]=fa2; if (fa2) Son[fa2][Son[fa2][1]==fa1]=x; Pushup(fa1); Pushup(x); }
Pushup函数是更新这个节点的值 具体看代码
void Pushup(int x) { if (x) { Size[x]=cnt[x]; if (Son[x][0]) Size[x]+=Size[Son[x][0]]; if (Son[x][1]) Size[x]+=Size[Son[x][1]]; } }
Get函数是得到左右儿子
bool Get(int Now) { return Son[fa[Now]][1]==Now; }
note:记得祖孙三代的值都要更新
cnt存在是因为二叉搜索树上不能有重复的数,于是我们就……在遇到同样的数的时候把cnt++!
好的 灵魂操作解决了接下来就相对轻松啦!
splay操作
splay操作的实质其实就是把一个节点一路旋转到目标节点
唯一需要注意的点是如果出现祖孙三代三点同在一条线上的时候,先旋转父亲再旋转自己,否则旋转两次自己
代码:
void Splay(int Now,int goal) { for (int Fa;(Fa=fa[Now])!=goal;rotate(Now)) if (fa[Fa]!=goal) if (Get(Now)==Get(Fa)) rotate(Fa); else rotate(Now); if (!goal) rt=Now; }
!goal的时候表示Now这个节点旋到根节点去了……
insert操作
insert操作的时候需要分类讨论一下
如果这是这棵树里面唯一的节点,这个点就是根
如果这个值已经在树里出现过了,那么就把它的cnt++
否则就和二叉搜索树一样把它插入到相应的位置就OK了
代码:
void Insert(int Now) { if (rt==0) { size++; key[size]=Now; rt=size; cnt[size]=Size[size]=1; fa[size]=Son[size][0]=Son[size][1]=0; return; } int now=rt,Fa=0; while (1) { if (Now==key[now]) { cnt[now]++; Pushup(now);Pushup(Fa); Splay(now); return; } Fa=now;now=Son[now][key[now]<Now]; if (now==0) { size++; Size[size]=cnt[size]=1; Son[size][0]=Son[size][1]=0; Son[Fa][Now>key[Fa]]=size; fa[size]=Fa; key[size]=Now; Pushup(Fa); Splay(size); return; } } }
动态求解K大和求解K在树中的排名
这两个操作是有点对称的……
有点像权值线段树的操作,如果左边够就往左边走,否则就把左边的扣掉往右边走
具体直接看代码应该挺好理解的?
K大:
int Kth(int Now) { int now=rt; while (1) { if (Son[now][0]&&Now<=Size[Son[now][0]]) now=Son[now][0]; else { int temp=Size[Son[now][0]]+cnt[now]; if (Now<=temp) return key[now]; Now-=temp; now=Son[now][1]; } } }
rank:
int Rank(int Now) { int now=rt,ans=0; while (1) { if (Now<key[now]) now=Son[now][0]; else { ans+=Size[Son[now][0]]; if (Now==key[now]) { Splay(now); return ans+1; } ans+=cnt[now]; now=Son[now][1]; } } }
求解前驱和后继
因为splay完之后这个点的已经在根上了,它的前驱就是这个左子树中最右边的那个,后继就是柚子树右子树的最左边那个orz
pre:
int Pre() { int now=Son[rt][0]; while (Son[now][1]) now=Son[now][1]; return now; }
在求解前驱之前记得先把这个点插入进树里 顺便把他旋转到根
if (opt==5) { Insert(x); printf("%d\n",key[Pre()]); del(x); }
nex:
int nex() { int now=Son[rt][1]; while (Son[now][0]) now=Son[now][0]; return now; }
同理在主程序中的处理:
if (opt==6) { Insert(x); printf("%d\n",key[nex()]); del(x); }
delete操作
这是个比较麻烦的操作,所以我会尽量讲的清楚点orz
和insert一样,我们还是考虑分类讨论
先把要被删除的节点旋到根
如果树中被删除的值不止一个 直接把cnt--就完事了
如果被删除的数是树中唯一的值,把它删掉,树置为空
如果被删除的树只有左子树或者右子树,把它删了把左子树/右子树换成根就完事了
如果被删除的树同时有左右子树就先把它删了,把左子树旋到根上,然后把原本的右子树扔个新的根节点
然后分这四种情况讨论就OK啦
代码:
void del(int Now) { Rank(Now); if (cnt[rt]>1) { cnt[rt]--; Pushup(rt); return; } if (!Son[rt][0]&&!Son[rt][1]) { Clear(rt);rt=0;return; } if (!Son[rt][0]) { int Ort=rt; rt=Son[rt][1]; fa[rt]=0; Clear(Ort); return; } else if (!Son[rt][1]) { int Ort=rt; rt=Son[rt][0]; fa[rt]=0; Clear(Ort); return; } int Ort=rt; int Lb=Pre(); Splay(Lb); Son[rt][1]=Son[Ort][1]; fa[Son[Ort][1]]=rt; Clear(Ort); Pushup(Ort); }
code[写的很丑]
#include <bits/stdc++.h> using namespace std; int Size[1000005],size; int Son[1000005][2],cnt[1000005],fa[1000005],rt,key[1000005]; void Clear(int x) { fa[x]=0; cnt[x]=0; key[x]=0; Son[x][0]=Son[x][1]=0; Size[x]=0; } void Pushup(int x) { if (x) { Size[x]=cnt[x]; if (Son[x][0]) Size[x]+=Size[Son[x][0]]; if (Son[x][1]) Size[x]+=Size[Son[x][1]]; } } bool Get(int Now) { return Son[fa[Now]][1]==Now; } void rotate(int x) { int fa1=fa[x]; int fa2=fa[fa1]; int K=Get(x); int K1=Get(fa1); Son[fa1][K]=Son[x][K^1]; fa[Son[fa1][K]]=fa1; Son[x][K^1]=fa1; fa[fa1]=x; fa[x]=fa2; if (fa2) Son[fa2][Son[fa2][1]==fa1]=x; Pushup(fa1); Pushup(x); } void Splay(int Now) { for (int Fa;Fa=fa[Now];rotate(Now)) if (fa[Fa]) if (Get(Now)==Get(Fa)) rotate(Fa); else rotate(Now); rt=Now; } void Insert(int Now) { if (rt==0) { size++; key[size]=Now; rt=size; cnt[size]=Size[size]=1; fa[size]=Son[size][0]=Son[size][1]=0; return; } int now=rt,Fa=0; while (1) { if (Now==key[now]) { cnt[now]++; Pushup(now);Pushup(Fa); Splay(now); return; } Fa=now;now=Son[now][key[now]<Now]; if (now==0) { size++; Size[size]=cnt[size]=1; Son[size][0]=Son[size][1]=0; Son[Fa][Now>key[Fa]]=size; fa[size]=Fa; key[size]=Now; Pushup(Fa); Splay(size); return; } } } int Rank(int Now) { int now=rt,ans=0; while (1) { if (Now<key[now]) now=Son[now][0]; else { ans+=Size[Son[now][0]]; if (Now==key[now]) { Splay(now); return ans+1; } ans+=cnt[now]; now=Son[now][1]; } } } int Kth(int Now) { int now=rt; while (1) { if (Son[now][0]&&Now<=Size[Son[now][0]]) now=Son[now][0]; else { int temp=Size[Son[now][0]]+cnt[now]; if (Now<=temp) return key[now]; Now-=temp; now=Son[now][1]; } } } int Pre() { int now=Son[rt][0]; while (Son[now][1]) now=Son[now][1]; return now; } int nex() { int now=Son[rt][1]; while (Son[now][0]) now=Son[now][0]; return now; } void del(int Now) { Rank(Now); if (cnt[rt]>1) { cnt[rt]--; Pushup(rt); return; } if (!Son[rt][0]&&!Son[rt][1]) { Clear(rt);rt=0;return; } if (!Son[rt][0]) { int Ort=rt; rt=Son[rt][1]; fa[rt]=0; Clear(Ort); return; } else if (!Son[rt][1]) { int Ort=rt; rt=Son[rt][0]; fa[rt]=0; Clear(Ort); return; } int Ort=rt; int Lb=Pre(); Splay(Lb); Son[rt][1]=Son[Ort][1]; fa[Son[Ort][1]]=rt; Clear(Ort); Pushup(Ort); } int main() { int N; scanf("%d",&N); while (N--) { int opt,x; scanf("%d%d",&opt,&x); if (opt==1) Insert(x); if (opt==2) del(x); if (opt==3) printf("%d\n",Rank(x)); if (opt==4) printf("%d\n",Kth(x)); if (opt==5) { Insert(x); printf("%d\n",key[Pre()]); del(x); } if (opt==6) { Insert(x); printf("%d\n",key[nex()]); del(x); } } return 0; }
题目是luogu P3369,只涉及到最基本的平衡树操作
区间翻转和lazy tag,下传tag
我们要翻转一个序列上的[l,r]区间,在splay上要怎么做呢?
考虑以区间的下标建立起splay,无论怎么变动,中序遍历的区间下标是不变的qwq
然后考虑交换splay每个点上的数字
如果暴力修改是O(N)[大概?]的效率
我们需要更加优秀的修改方式
想想线段树怎么处理这种问题的?
lazy tag呀!
为了方便我们多加入两个虚拟节点-inf 和 inf
要修改的区间就是[l+1,r+1]
但是这些区间不在同一个子树下 tag打不上可咋整啊?
那就把他们旋转到同一个子树下!
把r+2旋转到根
再把l旋到r+2的左儿子 这时候[l+1,r+1]就会在根节点的右子树的左子树下了
可以稍微画图理解一下……
然后对于这玩意儿打上tag就行了
然后insert数据的时候还有一个小trick
如果一个个插入未免太慢了,我们考虑类似于线段树的build去建立一棵splay
具体实现细节可以看代码qwq
#include <bits/stdc++.h> using namespace std; int Size[1000005],size; int Son[1000005][2],cnt[1000005],data[1000005],fa[1000005],rt,key[1000005],tag[10000005]; void Pushup(int x) { Size[x]=1; Size[x]+=Size[Son[x][0]]; Size[x]+=Size[Son[x][1]]; } void PushDown(int x) { if (x&&tag[x]) { tag[Son[x][0]]^=1; tag[Son[x][1]]^=1; swap(Son[x][0],Son[x][1]); tag[x]=0; } } bool Get(int Now) { return Son[fa[Now]][1]==Now; } void rotate(int x) { int fa1=fa[x]; int fa2=fa[fa1]; PushDown(x); PushDown(fa1); int K=Get(x); int K1=Get(fa1); Son[fa1][K]=Son[x][K^1]; fa[Son[fa1][K]]=fa1; Son[x][K^1]=fa1; fa[fa1]=x; fa[x]=fa2; if (fa2) Son[fa2][Son[fa2][1]==fa1]=x; Pushup(fa1); Pushup(x); } void Splay(int Now,int goal) { for (int Fa;(Fa=fa[Now])!=goal;rotate(Now)) if (fa[Fa]!=goal) if (Get(Now)==Get(Fa)) rotate(Fa); else rotate(Now); if (!goal) rt=Now; } int Kth(int Now) { int now=rt; while (1) { PushDown(now); if (Now<=Size[Son[now][0]]) now=Son[now][0]; else if (Now>Size[Son[now][0]]) { Now-=Size[Son[now][0]]+1; if (!Now) return now; now=Son[now][1]; } } } void Reverse(int l,int r) { l=Kth(l); r=Kth(r+2); Splay(l,0); Splay(r,l); PushDown(rt); tag[Son[Son[rt][1]][0]]^=1; } void Write(int Now) { PushDown(Now); if (Son[Now][0]) Write(Son[Now][0]); if (key[Now]!=-1e9&&key[Now]!=1e9)printf("%d ",key[Now]); if (Son[Now][1]) Write(Son[Now][1]); } int Build(int Fa,int l,int r) { if (l>r) return 0; int mid=(l+r)>>1; int Now=++size; key[Now]=data[mid]; fa[Now]=Fa; tag[Now]=0; Son[Now][0]=Build(Now,l,mid-1); Son[Now][1]=Build(Now,mid+1,r); Pushup(Now); return Now; } int main() { int N,M; scanf("%d%d",&N,&M); data[1]=-1e9; for (int i=1;i<=N;i++) data[i+1]=i; data[N+2]=1e9; rt=Build(0,1,N+2); while (M--) { int l,r; scanf("%d%d",&l,&r); Reverse(l,r); } Write(rt); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步