平衡树(Splay)
参考自:OI Wiki
伸展树
定义
Splay 树, 或 伸展树,是一种平衡二叉查找树,它通过 Splay/伸展操作 不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,能够在均摊 时间内完成插入,查找和删除操作,并且保持平衡而不至于退化为链。
Splay 树由 Daniel Sleator 和 Robert Tarjan 于 1985 年发明。
结构
二叉查找树的性质
首先肯定是一棵二叉树!
能够在这棵树上查找某个值的性质:左子树任意节点的值 根节点的值 右子树任意节点的值。
节点维护信息
root | tot | fa[i] | lc[i]/rc[i] | val[i] | cnt[i] | si[i] |
---|---|---|---|---|---|---|
根节点编号 | 节点个数 | 父亲 | 左右儿子编号 | 节点权值 | 权值出现次数 | 子树大小 |
操作
基本操作
siz(x)
:在改变节点位置后,将节点x的size更新。wh(x)
:判断节点 是父亲节点的左儿子还是右儿子。
inline bool wh(int x)//判断是否为右子树; (具体用法见操作splay) { return rc[fa[x]]==x; } inline void siz(int x)//计算子树大小size { si[x]=si[lc[x]]+si[rc[x]]+cnt[x]; }
旋转操作
为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。
旋转需要保证:
- 整棵 Splay 的中序遍历不变(不能破坏二叉查找树的性质)。
- 受影响的节点维护的信息依然正确有效。
root
必须指向旋转后的根节点。
在 Splay 中旋转分为两种:左旋和右旋。
inline void xz(int x)//旋转 (保证中序遍历不变) { int y=fa[x],z=fa[y]; int a=(rc[y]==x?lc[x]:rc[x]); fa[x]=z,fa[y]=x; if(a) fa[a]=y; if(z) (rc[z]==y?rc[z]:lc[z])=x; if(rc[y]==x) lc[x]=y,rc[y]=a; else rc[x]=y,lc[y]=a; siz(y); }
Splay 操作
Splay 操作规定:每访问一个节点 后都要强制将其旋转到根节点。
inline void splay(int x,int ro)//旋到对应位置 { while(fa[x]!=ro) { if(fa[fa[x]]!=ro) wh(fa[x])==wh(x)?xz(fa[x]):xz(x);//同为右(或左)则旋父节点,反之旋自身; xz(x); } if(!ro) root=x; siz(x); }
插入操作
过程
插入操作是一个比较复杂的过程,具体步骤如下(假设插入的值为x):
- 如果树空了,则直接插入根并退出。
- 如果当前节点的权值等于x则增加当前节点的大小并更新节点和父亲的信息,将当前节点进行 Splay 操作。
- 否则按照二叉查找树的性质向下找,找到空节点就插入即可(请不要忘记 Splay 操作)。
inline void insert(int v)//插入 { int x=root,y=0,pd; while(val[x]!=v&&x) { si[y=x]++; if(val[x]>v) x=lc[x],pd=0; else x=rc[x],pd=1; } if(!x) { x=++tot; val[x]=v,fa[x]=y; if(y) (!pd?lc[y]:rc[y])=x; } si[x]++,cnt[x]++; splay(x,0); }
合并两棵树
过程
合并两棵 Splay 树,设两棵树的根节点分别为x和y,那么我们要求x树中的最大值小于y树中的最小值。合并操作如下:
- 如果x和y其中之一或两者都为空树,直接返回不为空的那一棵树的根节点或空树。
- 否则将x树中的最大值splay到根,然后把它的右子树设置为y并更新节点的信息,然后返回这个节点。
inline void find(int x)//找到值x对应节点并旋成根 { int y=root; while(val[y]!=x) y=(x<val[y]?lc[y]:rc[y]); splay(y,0); } inline int fmax(int x)//找到以x为根的子树的最大值 { while(rc[x]) x=rc[x]; return x; } inline void join(int x,int y)//合并两棵树 { int z=fmax(x); splay(z,0); rc[z]=y,fa[y]=z; siz(z); }
删除操作
过程
删除操作也是一个比较复杂的操作,具体步骤如下:
首先将 旋转到根的位置。
- 如果 (有不止一个 ),那么将 减 并退出。
- 否则,合并它的左右两棵子树即可。
inline void de(int x)//删点 { find(x); if(cnt[root]>1)//该点值的数量大于1,删一个 { cnt[root]--,si[root]--; return; } if(si[root]==1)//只有该点,重置树 root=0; else if(!lc[root]||!rc[root])//去掉该点,根变为右(或左)儿子 root=lc[root]+rc[root],fa[root]=0; else//删除该点,合并两棵子树 { fa[lc[root]]=fa[rc[root]]=0; join(lc[root],rc[root]); } }
查询前驱
过程
前驱定义为小于x的最大的数,那么查询前驱可以转化为:将x插入(此时x已经在根的位置了),前驱即为x的左子树中最右边的节点,最后将x删除即可。
inline int qq(int x)//找前驱 { insert(x); int y=lc[root]; while(rc[y]) y=rc[y]; de(x); return y; }
查询后继
过程
后继定义为大于x的最小的数,查询方法和前驱类似:x的右子树中最左边的节点。
inline int hj(int x)//找后继 { insert(x); int y=rc[root]; while(lc[y]) y=lc[y]; de(x); return y; }
查询 x 的排名
过程
在伸展树中找到x并旋到根(找不到则插入,处理完后删除),返回左子树的size+1;
inline void find(int x)//找到值x对应节点并旋成根 { int y=root; while(val[y]!=x) y=(x<val[y]?lc[y]:rc[y]); splay(y,0); } inline int pm(int x)//查x的排名 { find(x); return si[lc[root]]+1; }
查询排名 x 的数
过程
设x为剩余排名,具体步骤如下:
- 如果左子树非空且剩余排名x不大于左子树的大小size,那么向左子树查找。
- 否则将x减去左子树的和根的大小。如果此时k的值小于等于 ,则返回根节点的权值,否则继续向右子树查找。
inline int q(int x)//查排名为x的值 { int y=root; for(;;) { if(x<si[lc[y]]+1) y=lc[y]; else if(x>si[lc[y]]+cnt[y]) x-=si[lc[y]]+cnt[y],y=rc[y]; else return y; } }
P3369 【模板】普通平衡树
模板题
#include<bits/stdc++.h> using namespace std; const int N=1e5+5; int tot,root,n; int fa[N],lc[N],rc[N],val[N],cnt[N],si[N]; inline bool wh(int x)//判断是否为右子树; (具体用法见操作splay) { return rc[fa[x]]==x; } inline void siz(int x)//计算子树大小size { si[x]=si[lc[x]]+si[rc[x]]+cnt[x]; } inline void xz(int x)//旋转 (保证中序遍历不变) { int y=fa[x],z=fa[y]; int a=(rc[y]==x?lc[x]:rc[x]); fa[x]=z,fa[y]=x; if(a) fa[a]=y; if(z) (rc[z]==y?rc[z]:lc[z])=x; if(rc[y]==x) lc[x]=y,rc[y]=a; else rc[x]=y,lc[y]=a; siz(y); } inline void splay(int x,int ro)//旋到对应位置 { while(fa[x]!=ro) { if(fa[fa[x]]!=ro) wh(fa[x])==wh(x)?xz(fa[x]):xz(x);//同为右(或左)则旋父节点,反之旋自身; xz(x); } if(!ro) root=x; siz(x); } inline void insert(int v)//插入 { int x=root,y=0,pd; while(val[x]!=v&&x) { si[y=x]++; if(val[x]>v) x=lc[x],pd=0; else x=rc[x],pd=1; } if(!x) { x=++tot; val[x]=v,fa[x]=y; if(y) (!pd?lc[y]:rc[y])=x; } si[x]++,cnt[x]++; splay(x,0); } inline void find(int x)//找到值x对应节点并旋成根 { int y=root; while(val[y]!=x) y=(x<val[y]?lc[y]:rc[y]); splay(y,0); } inline int fmax(int x)//找到以x为根的子树的最大值 { while(rc[x]) x=rc[x]; return x; } inline void join(int x,int y)//合并两棵树 { int z=fmax(x); splay(z,0); rc[z]=y,fa[y]=z; siz(z); } inline void de(int x)//删点 { find(x); if(cnt[root]>1)//该点值的数量大于1,删一个 { cnt[root]--,si[root]--; return; } if(si[root]==1)//只有该点,重置树 root=0; else if(!lc[root]||!rc[root])//去掉该点,根变为右(或左)儿子 root=lc[root]+rc[root],fa[root]=0; else//删除该点,合并两棵子树 { fa[lc[root]]=fa[rc[root]]=0; join(lc[root],rc[root]); } } inline int qq(int x)//找前驱 { insert(x); int y=lc[root]; while(rc[y]) y=rc[y]; de(x); return y; } inline int hj(int x)//找后继 { insert(x); int y=rc[root]; while(lc[y]) y=lc[y]; de(x); return y; } inline int pm(int x)//查x的排名 { find(x); return si[lc[root]]+1; } inline int q(int x)//查排名为x的值 { int y=root; for(;;) { if(x<si[lc[y]]+1) y=lc[y]; else if(x>si[lc[y]]+cnt[y]) x-=si[lc[y]]+cnt[y],y=rc[y]; else return y; } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { int a,b; scanf("%d%d",&a,&b); if(a==1) insert(b); if(a==2) de(b); if(a==3) printf("%d\n",pm(b)); if(a==4) printf("%d\n",val[q(b)]); if(a==5) printf("%d\n",val[qq(b)]); if(a==6) printf("%d\n",val[hj(b)]); } return 0; }
此外:
splay还可以支持区间翻转操作。
这就需要懒标记出马了:la[i]
懒标记下传
inline void pu(int x)//懒标记下传 { if(x&&la[x]) { swap(lc[x],rc[x]); la[lc[x]]^=1; la[rc[x]]^=1; la[x]=0; } }
建树
inline int bu(int l,int r,int f)//以基础区间建立伸展树 { if(l>r) return 0; int mid=(l+r)>>1,x=++tot; fa[x]=f,val[x]=a[mid]; lc[x]=bu(l,mid-1,x),rc[x]=bu(mid+1,r,x); siz(x); return x; }
旋转和splay操作是一样的
inline bool wh(int x)
{
return lc[fa[x]]==x;
}
inline void xz(int x) { int y=fa[x],z=fa[y]; int a=(lc[y]==x?rc[x]:lc[x]); fa[x]=z,fa[y]=x; if(a) fa[a]=y; if(z) (lc[z]==y?lc[z]:rc[z])=x; if(lc[y]==x) rc[x]=y,lc[y]=a; else lc[x]=y,rc[y]=a; siz(y); } inline void splay(int x,int ro) { while(fa[x]!=ro) { if(fa[fa[x]]!=ro) wh(fa[x])==wh(x)?xz(fa[x]):xz(x); xz(x); } siz(x); if(!ro) root=x; }
假设翻转区间[l,r]
则先将l-1旋转到根,再将r+1旋转为根的右儿子。此时,需要翻转的区间就是r+1的左子树,设置懒标记。
另外,我们还需要在整个序列两端加上边界。
假设序列a,那么a[1]=_INT_MAX,a[n+1]=INT_MAX。此时,由于a[1]不再是真正序列的起始位置,所以翻转的区间应为[l+1,r+1]。
inline void siz(int x) { si[x]=si[lc[x]]+si[rc[x]]+1; } inline int find(int x)//找到对应节点 { int y=root; pu(y); while(x!=si[lc[y]]+1) { if(x<si[lc[y]]+1) y=lc[y],pu(y); else x-=si[lc[y]]+1,y=rc[y],pu(y); } return y; } int main() { scanf("%d%d",&n,&m); a[1]=-INT_MAX,a[n+2]=INT_MAX; for(int i=1;i<=n;i++) a[i+1]=i; root=bu(1,n+2,0); for(int i=1;i<=m;i++) { int l,r; scanf("%d%d",&l,&r); splay(find(l),0); splay(find(r+2),root); la[lc[rc[root]]]^=1; } print(root); }
P3391 【模板】文艺平衡树
模板题
#include<bits/stdc++.h> using namespace std; const int N=1e5+5; int n,m,tot,root; int a[N],si[N],val[N],fa[N],lc[N],rc[N]; bool la[N]; inline bool wh(int x) { return lc[fa[x]]==x; } inline void siz(int x) { si[x]=si[lc[x]]+si[rc[x]]+1; } inline void pu(int x)//懒标记下传 { if(x&&la[x]) { swap(lc[x],rc[x]); la[lc[x]]^=1; la[rc[x]]^=1; la[x]=0; } } inline int bu(int l,int r,int f)//以基础区间建立伸展树 { if(l>r) return 0; int mid=(l+r)>>1,x=++tot; fa[x]=f,val[x]=a[mid]; lc[x]=bu(l,mid-1,x),rc[x]=bu(mid+1,r,x); siz(x); return x; } inline void xz(int x) { int y=fa[x],z=fa[y]; int a=(lc[y]==x?rc[x]:lc[x]); fa[x]=z,fa[y]=x; if(a) fa[a]=y; if(z) (lc[z]==y?lc[z]:rc[z])=x; if(lc[y]==x) rc[x]=y,lc[y]=a; else lc[x]=y,rc[y]=a; siz(y); } inline void splay(int x,int ro) { while(fa[x]!=ro) { if(fa[fa[x]]!=ro) wh(fa[x])==wh(x)?xz(fa[x]):xz(x); xz(x); } siz(x); if(!ro) root=x; } inline int find(int x)//找到对应节点 { int y=root; pu(y); while(x!=si[lc[y]]+1) { if(x<si[lc[y]]+1) y=lc[y],pu(y); else x-=si[lc[y]]+1,y=rc[y],pu(y); } return y; } inline void print(int x) { pu(x); if(lc[x]) print(lc[x]); if(val[x]^-INT_MAX&&val[x]^INT_MAX) printf("%d ",val[x]); if(rc[x]) print(rc[x]); } int main() { scanf("%d%d",&n,&m); a[1]=-INT_MAX,a[n+2]=INT_MAX; for(int i=1;i<=n;i++) a[i+1]=i; root=bu(1,n+2,0); for(int i=1;i<=m;i++) { int l,r; scanf("%d%d",&l,&r); splay(find(l),0); splay(find(r+2),root); la[lc[rc[root]]]^=1; } print(root); }
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术