Splay

Splay 是一种二叉查找树,它通过不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,并且保持平衡而不至于退化为链,它由 Daniel Sleator 和 Robert Tarjan 发明。

下面的代码可以通过模板题 Luogu P3369

结构

二叉搜索树的性质

左子树任意节点的值 < 根节点的值 < 右子树任意节点的值

节点维护信息

操作

基本操作

inline void maintain(int a){
	size[a]=size[child[a][0]]+size[child[a][1]]+count[a];
}
inline bool get(int a){
	return a==child[fa[a]][1];
}
inline void clear(int a){
	fa[a]=child[a][0]=child[a][1]=value[a]=count[a]=size[a]=0;
}

旋转操作

为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。

要求

步骤

C++
inline void rotate(int a){ bool check=get(a); int b=fa[a],c=fa[b]; child[b][check]=child[a][!check]; fa[child[a][!check]]=b; child[a][!check]=b; fa[b]=a; fa[a]=c; if(c)child[c][b==child[c][1]]=a; maintain(b); maintain(a); }

Splay 操作

Splay 规定:每访问一个节点后都要强制将其旋转到根节点。此时旋转操作具体分为 6 种情况讨论(其中 $a$ 为需要旋转到根的节点)

C++
inline void splay(int a){ for(int i=fa[a];i=fa[a],i;rotate(a)) if(fa[i]) rotate(get(a)==get(i)?i:a); root=a; }

插入操作

如果要在 Splay 树中插入值为 $k$ 的节点,需要经过一下步骤:

C++
inline void ins(int k){ if(!root){ value[++total]=k; count[total]++; root=total; maintain(root); return; } int cnr=root,f=0; while(true){ if(value[cnr]==k){ count[cnr]++; maintain(cnr); maintain(f); splay(cnr); break; } f=cnr; cnr=child[cnr][value[cnr]<k]; if(!cnr){ value[++total]=k; count[total]++; fa[total]=f; child[f][value[f]<k]=total; maintain(total); maintain(f); splay(total); break; } } }

查询 $k$ 的排名

根据二叉搜索树的定义和性质,显然可以按照以下步骤查询 $k$ 的排名:

C++
inline int rank(int k){ int res=0,cnr=root; while(true){ if(k<value[cnr])cnr=child[cnr][0]; else{ res+=size[child[cnr][0]]; if(k==value[cnr]){ splay(cnr); return res+1; } res+=count[cnr]; cnr=child[cnr][1]; } } }

查询排名 $k$ 的数

设 $k$ 为剩余排名,具体步骤如下:

C++
inline int kth(int k){ int cnr=root; while(true){ if(child[cnr][0]&&k<=size[child[cnr][0]])cnr=child[cnr][0]; else{ k-=count[cnr]+size[child[cnr][0]]; if(k<=0){ splay(cnr); return value[cnr]; } cnr=child[cnr][1]; } } }

查询前驱

$a$ 的前驱可以转化为:$a$ 的左子树中最右边的节点,求法如下:

C++
inline int pre(){ int cnr=child[root][0]; while(child[cnr][1])cnr=child[cnr][1]; splay(cnr); return cnr; }

查询后继

$a$ 的后驱可以转化为:$a$ 的右子树中最左边的节点,求法如下:

C++
inline int next(){ int cnr=child[root][1]; while(child[cnr][0])cnr=child[cnr][0]; splay(cnr); return cnr; }

合并操作

合并两棵 Splay 树,设两棵树的根节点分别为 $a$ 和 $b$,那么我们要求 $a$ 树中的最大值小于 $b$ 树中的最小值。删除操作如下:

删除操作

删除操作步骤如下:

C++
inline void del(int k){ rank(k); if(count[root]>1){ count[root]--; maintain(root); return; } if(!child[root][0]&&!child[root][1]){ clear(root); root=0; return; } if(!child[root][0]){ int cnr=root; root=child[root][1]; fa[root]=0; clear(cnr); return; } if(!child[root][1]){ int cnr=root; root=child[root][0]; fa[root]=0; clear(cnr); return; } int cnr=root; int x=pre(); splay(x); fa[child[cnr][1]]=x; child[x][1]=child[cnr][1]; clear(cnr); maintain(root); }

完整代码

数组实现

提交记录 备份

结构体指针实现

如果写挂了,询问自己:

  1. 输入的数字 包含负数,我用的快读判断了负数吗?
  2. 对于使用指针的实现,如何处理空指针?是否避免了访问 NULL?有没有特别地清空初始的空节点?
  3. 插入的时候,假设当前节点是 $u$,我们要 maintain(u),也要 maintain(u->fa)
  4. 每次操作后,都要进行 splay 操作!对于其他的平衡树题目(尤其是树套树),求 rank 的时候可能存在不存在的情况,那时候也要 splay!
  5. rotate 操作的细节是否正确?

提交记录 备份

例题

名称 编号 备注 题解
【模板】普通平衡树 Luogu P3369 模板题 题解见上
郁闷的出纳员 NOI2004D1T1 全局 tag,将小于某个值的数全部删除 提交记录 备份
文艺平衡树 LOJ 105 区间翻转,Splay 上打 tag,利用 Splay 操作后子树的性质
用 Splay 方便维护区间翻转操作的原因是,可以通过 splay 操作使得要操作的区间变成一棵子树,
打个标记即可,而下一次操作又能很自然地将另一个区间聚成一课子树。
提交记录 备份
[Lydsy1706月赛]K小值查询 BZOJ 4923 维护一个长度为 $n$ 的正整数序列 $a_1,a_2,\cdots,a_n$,支持以下两种操作:
1 k,将序列 $a$ 从小到大排序,输出 $a_k$ 的值。
2 k,将所有严格大于 $k$ 的数 $a_i$ 减去 $k$。
将询问的权值分成 $[0,c)$、$[c,2c)$ 和 $[2c,+\infty)$ 三段,将第三个部分打 tag 减去 $c$,
将第二个暴力插入第一个,势能分析时间复杂度,每个数被暴力操作时至少 $\div 2$,时间复杂度为 $O((n+m) \log n \log \max\{a_i\})$。
提交记录 备份
T-Shirts CF702F 平衡树,将询问的权值分成 $[0,c)$、$[c,2c)$ 和 $[2c,+\infty)$ 三段,将第三个部分打 tag 减去 $c$,
将第二个暴力插入第一个,势能分析时间复杂度
提交记录 备份

参考资料