Splay
学习自该博客
(dalao讲得真好,我只为了记忆写重点)
Splay是一种数据结构,用于在线调整二叉排序树的操作,用于让树平衡(空树或任意节点的左右两个子树的深度差的绝对值不超过1)转换根节点或者其他操作。
二叉排序树,左子树任意一个点的值小于根节点,右子树任意一个点大于根节点的树。很显然,如果你想把一群点构造成这样一个树,你可以构造出许多不同的、满足关系的树。
(以下所有图片来自网上)
比如下面两个树就是等价的:
不平衡 平衡
那么Splay要怎么旋转呢?大概是把要旋转的点更改到它的父亲节点的位置。我们发现,要满足关系,如果原来的点在父节点的右子树,旋转后父节点就连接在该节点的左子树上。反之亦然。大概是这样的:
差不多就是这样。
可以用结构体或者数组记录节点信息。我就写个结构体吧因为dalao的是用数组的。
struct tree{ int val,fa,son[2],cnt,siz; //val权 fa父节点 son[0]左节点 son[1]右节点 cnt出现次数 siz树大小(包括子树siz和自身cnt) }
int siz,root;//siz时间戳,root根节点(因为splay会更改根节点)
插入时用三种情况:空树,直接插入;查询到权相同的点,cnt++;没有查询到点,向下插入新点。
inline void insert(int x) { if(siz==0){ siz++; e[siz].son[1]=e[siz].son[0]=e[siz].fa=0; e[siz].siz=e[siz].cnt=1; root = 1; e[siz].val=x; return; } int now=root,f=0; while(1){ if(e[now].val==x){ e[now].cnt++; update(now); update(f); splay(now,0); break; } f=now; now=son[now][x>e[now].val]; if(!now){ siz++; e[siz].son[1]=e[siz].son[0]=0; e[siz].fa=f; e[siz].siz=e[siz].cnt=1; e[f].son[x>e[now].val]=siz; e[siz].val=x; update(f); splay(siz,0); break; } } }
如果没有重复的值,你也可以直接dfs建树。
int build_tree(int l,int r,int f){ if(l > r)return 0; int mid=l+r>>1; int now=++siz; e[now].fa=f; e[now].son[0]=e[now].son[1]=0; e[now].cnt++; e[now].val=a[mid]; e[now].siz++; e[now].son[0]=build_tree(l,mid-1,now); e[now].son[1]=build_tree(mid+1,r,now); update(now); return now; }
那么我们刚才说了那么久的旋转代码如下:
inline void rotate(int x) { int f=e[x].fa,ff=e[f].fa;//记录父节点和祖父节点 bool w=which(x);//splay[自动]旋转的奥秘,得到x的位置 //以下旋转共6步,就是分别记录三个点间的关系,便于记忆 e[f].son[w]=e[x].son[w^1]; e[e[f].son[w]].fa=f; e[f].fa=x; e[x].son[w^1]=f; e[x].fa=ff; if(ff) e[ff].son[e[ff].son[1]==f]=x; update(f); update(x); }
这里有个问题:如果连续的两条边方向相同,若只是将底部的点向上旋转会单旋使树失衡。那么我们只要在旋转之前把父节点旋转一下就好了。具体的图请看dalao博客(不想复制以防侵权)
goal是旋转的目标点,旋转后x节点会在goal的子节点。
inline void splay(int x,int goal){ for(int i;(i=e[x].fa)!=goal;rotate(x)) { if(e[i].fa!=goal){ rotate(which(x)==which(i)?i:x); } } if(goal==0) root=x; }
查询:查询点的位置,我们按从小到大查询。发现因为子树的siz是全部小于节点的,所以如果x(要查询的数)<=siz[son[0]],我们往左查询;否则x-=siz[son[0]],跳到右子树重复查询,直到x==0,返回查到的点就ok了。
inline int find(int x) { int now=root; while(1) { pushdown(now); if(x<=e[e[now].son[0]].siz) now=e[now].son[0]; else{ x-=e[e[now].son[0]].siz+1; if(!x)return now; now=e[now].son[1]; } } }
查询前驱和后继,就是查询和这个点左右最近的两个点,先把它变为根节点,再查询左子树的最右节点和右子树最左节点:
int findpre(int x)//注意x是编号而不是值,这个查前驱 {
splay(x,0); //注意变成根节点。如果不是那么下面都是假的 int now=son[x][0]; while(son[now][1])now=son[now][1]; return now; }
后缀一样(当然是相反)。
删除有点麻烦留坑待填.
大概的思路:
- if节点有多个数,即cnt>1,直接减去就好了。
- else if节点没有子树,直接删掉就好了。
- else if只有左子树,用左儿子替代之。
- else if只有右子树,用右儿子替代之。
- else if左右子树都有,我们把它的前驱作为新根,把它splay一下,然后把原来点的右儿子连成它的右儿子就OK了。
写个例题吧:
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<ctype.h> #define INF 0x3f3f3f3f using namespace std; inline int read() { int w=0,x=0;char c=getchar(); while(!isdigit(c))w|=c=='-',c=getchar(); while(isdigit(c))x=(x<<3)+(x<<1)+(c^48),c=getchar(); return w?-x:x; } namespace star { const int maxn=1e5+10; int n,m,root,siz,a[maxn]; struct node{ int son[2],siz,fa,tag,cnt,val; }e[maxn]; inline bool which(int x){return e[e[x].fa].son[1]==x;} inline void update(int x){ if(!x)return; e[x].siz=e[x].cnt; if(e[x].son[0])e[x].siz+=e[e[x].son[0]].siz; if(e[x].son[1])e[x].siz+=e[e[x].son[1]].siz; } inline void pushdown(int x){ if(x and e[x].tag){ e[e[x].son[0]].tag^=1; e[e[x].son[1]].tag^=1; e[x].tag=0; swap(e[x].son[0],e[x].son[1]); } } inline void rotate(int x) { int f=e[x].fa,ff=e[f].fa;//记录父节点和祖父节点 pushdown(x),pushdown(f); bool w=which(x);//splay[自动]旋转的奥秘,得到x的位置 //以下旋转共6步,就是分别记录三个点间的关系,便于记忆 e[f].son[w]=e[x].son[w^1]; e[e[f].son[w]].fa=f; e[f].fa=x; e[x].son[w^1]=f; e[x].fa=ff; if(ff) e[ff].son[e[ff].son[1]==f]=x; update(f); update(x); } inline void splay(int x,int goal){ for(int i;(i=e[x].fa)!=goal;rotate(x)) { if(e[i].fa!=goal){ rotate(which(x)==which(i)?i:x); } } if(goal==0) root=x; } int build_tree(int l,int r,int f){ if(l > r)return 0; int mid=l+r>>1; int now=++siz; e[now].fa=f; e[now].son[0]=e[now].son[1]=0; e[now].cnt++; e[now].val=a[mid]; e[now].siz++; e[now].son[0]=build_tree(l,mid-1,now); e[now].son[1]=build_tree(mid+1,r,now); update(now); return now; } inline int find(int x) { int now=root; while(1) { pushdown(now); if(x<=e[e[now].son[0]].siz) now=e[now].son[0]; else{ x-=e[e[now].son[0]].siz+1; if(!x)return now; now=e[now].son[1]; } } } inline void reverse(int x,int y) { int l=find(x-1),r=find(y+1); splay(l,0); splay(r,l); int pos=e[root].son[1]; pos=e[pos].son[0]; e[pos].tag^=1; } inline void dfs(int now){ pushdown(now); if(e[now].son[0])dfs(e[now].son[0]); if(e[now].val!=INF and e[now].val!=-INF)printf("%d ",e[now].val); if(e[now].son[1])dfs(e[now].son[1]); } /* inline void insert(int x) { if(siz==0){ siz++; e[siz].son[1]=e[siz].son[0]=e[siz].fa=0; e[siz].siz=e[siz].cnt=1; root = 1; e[siz].val=x; return; } int now=root,f=0; while(1){ if(e[now].val==x){ e[now].cnt++; update(now); update(f); splay(now,0); break; } f=now; now=son[now][x>e[now].val]; if(!now){ siz++; e[siz].son[1]=e[siz].son[0]=0; e[siz].fa=f; e[siz].siz=e[siz].cnt=1; e[f].son[x>e[now].val]=siz; e[siz].val=x; update(f); splay(siz,0); break; } } } */ inline void work() { n=read(),m=read(); a[1]=-INF,a[n+2]=INF; for(int i=1;i<=n;i++)a[i+1]=i; root=build_tree(1,n+2,0); for(int i=1;i<=m;i++){ int x=read(),y=read(); reverse(x+1,y+1); } dfs(root); } } int main() { star::work(); return 0; }
好长丫~~~
注意,每次更新完都splay是必要的,这会更新所有节点的siz值,否则是假的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现