Splay 树, 或 伸展树,是一种平衡二叉查找树,它通过 Splay/伸展操作 不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,能够在均摊 O(\log N) 时间内完成插入,查找和删除操作,并且保持平衡而不至于退化为链。
Splay 树由 Daniel Sleator 和 Robert Tarjan 于 1985 年发明。
code:
using namespace std;
const int maxn = 1e5+10;
int root,tot,fa[maxn],ch[maxn][2],val[maxn],cnt[maxn],sz[maxn];
int rt;
struct Splay{
void maintain(int x){
sz[x] = sz[ch[x][0]]+sz[ch[x][1]]+cnt[x];//在改变节点位置后,将节点x的size更新
}
bool get(int x){//判断x是父节点的左儿子还是右儿子
return x == ch[fa[x]][1];
}
void clear(int x){//销毁节点x
ch[x][0] = ch[x][1] = fa[x] = val[x] = sz[x] = cnt[x] = 0;
}
/*
将 y 的左儿子指向 x 的右儿子,且 x 的右儿子(如果 x 有右儿子的话)
的父亲指向 y;ch[y][0]=ch[x][1]; fa[ch[x][1]]=y;
将 x 的右儿子指向 y,且 y 的父亲指向 x;ch[x][chk^1]=y; fa[y]=x;
若原来的 y 还有父亲 z,那么把 z 的某个儿子(原来 y 所在的儿子位置)
指向 x,且 x 的父亲指向 z。fa[x]=z; if(z) ch[z][y==ch[z][1]]=x;
*/
void rotate(int x){
int y = fa[x],z = fa[y],chk = get(x);
ch[y][chk] = ch[x][chk^1];
if(ch[x][chk^1]) fa[ch[x][chk^1]] = y;
ch[x][chk^1] = y;
fa[y] = x;
fa[x] = z;
if(z) ch[z][y == ch[z][1]] = x;
maintain(y);
maintain(x);
}
/*
Splay 操作规定:每访问一个节点 x 后都要强制将其旋转到根节点。
Splay 操作即对 x 做一系列的 splay 步骤。每次对 x 做一次 splay 步
骤,x 到根节点的距离都会更近。定义 p 为 x 的父节点。Splay 步骤有
三种,具体分为六种情况:
*/
void splay(int x){
for(int f = fa[x];f = fa[x],f;rotate(x)){
if(fa[f]) rotate(get(x) == get(f)?f:x);
}
rt = x;
}
/*
如果树空了,则直接插入根并退出。
如果当前节点的权值等于 k 则增加当前节点的大小
并更新节点和父亲的信息,将当前节点进行 Splay 操作。
否则按照二叉查找树的性质向下找,找到空节点就插入即
可(请不要忘记 Splay 操作)。
*/
void ins(int k){
if(!rt){
val[++tot] = k;
cnt[tot]++;
rt = tot;
maintain(rt);
return;
}
int cur = rt,f = 0;
while(1){
if(val[cur] == k){
cnt[cur]++;
maintain(cur);
maintain(f);
splay(cur);
break;
}
f = cur;
cur = ch[cur][val[cur]<k];
if(!cur){
val[++tot] = k;
cnt[tot]++;
fa[tot] = f;
ch[f][val[f]<k] = tot;
maintain(f);
maintain(tot);
splay(tot);
break;
}
}
}
/*
如果 x 比当前节点的权值小,向其左子树查找。
如果 x 比当前节点的权值大,将答案加上左子树(size)和当前节点(cnt)的大小,向其右子树查找。
如果 x 与当前节点的权值相同,将答案加 1 并返回。
*/
int rk(int k){
int res = 0,cur = rt;
while(1){
if(k < val[cur]){
cur = ch[cur][0];
}
else{
res += sz[ch[cur][0]];
if(!cur) return res+1;
if(k == val[cur]){
splay(cur);
return res+1;
}
res += cnt[cur];
cur = ch[cur][1];
}
}
}
/*
如果左子树非空且剩余排名 k 不大于左子树的大小 size,那么向左子树查找。
否则将 k 减去左子树的和根的大小。如果此时 k 的值小于等于 0,则返回根节点
的权值,否则继续向右子树查找。
*/
int kth(int k) {
int cur = rt;
while (1) {
if (ch[cur][0] && k <= sz[ch[cur][0]]) {
cur = ch[cur][0];
}
else {
k -= cnt[cur] + sz[ch[cur][0]];
if (k <= 0) {
splay(cur);
return val[cur];
}
cur = ch[cur][1];
}
}
}
/*
前驱定义为小于 x 的最大的数,那么查询前驱可以转化为:将 x 插入(
此时 x 已经在根的位置了),前驱即为 x 的左子树中最右边的节点,最后将 x 删除即可。
*/
int pre(){
int cur = ch[rt][0];
if(!cur) return cur;
while(ch[cur][1]) cur = ch[cur][1];
splay(cur);
return cur;
}
int nex(){//后继
int cur = ch[rt][1];
if (!cur) return cur;
while(ch[cur][0]) cur = ch[cur][0];
splay(cur);
return cur;
}
/*
首先将 x 旋转到根的位置。
如果 cnt[x]>1(有不止一个 x),那么将 cnt[x] 减 1 并退出。
否则,合并它的左右两棵子树即可。
合并:
如果 x 和 y 其中之一或两者都为空树,直接返回不为空的那一棵树的根节点或空树。
否则将 x 树中的最大值 \operatorname{Splay} 到根,然后把它的右子树设置为 y 并
更新节点的信息,然后返回这个节点。
*/
void del(int k) {
rk(k);
if (cnt[rt] > 1) {
cnt[rt]--;
maintain(rt);
return;
}
if (!ch[rt][0] && !ch[rt][1]) {
clear(rt);
rt = 0;
return;
}
if (!ch[rt][0]) {
int cur = rt;
rt = ch[rt][1];
fa[rt] = 0;
clear(cur);
return;
}
if (!ch[rt][1]) {
int cur = rt;
rt = ch[rt][0];
fa[rt] = 0;
clear(cur);
return;
}
int cur = rt;
int x = pre();
fa[ch[cur][1]] = x;
ch[x][1] = ch[cur][1];
clear(cur);
maintain(rt);
}
}tree;
int n;
int main(){
cin>>n;
while(n--){
int opt,x;
cin>>opt>>x;
if (opt == 1)
tree.ins(x);
else if (opt == 2)
tree.del(x);
else if (opt == 3)
printf("%d\n", tree.rk(x));
else if (opt == 4)
printf("%d\n", tree.kth(x));
else if (opt == 5)
tree.ins(x), printf("%d\n", val[tree.pre()]), tree.del(x);
else
tree.ins(x), printf("%d\n", val[tree.nex()]), tree.del(x);
}
return 0;
}
旋转操作
为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。
旋转需要保证:
整棵 Splay 的中序遍历不变(不能破坏二叉查找树的性质)。
受影响的节点维护的信息依然正确有效。
root 必须指向旋转后的根节点。
在 Splay 中旋转分为两种:左旋和右旋。
过程
具体分析旋转步骤(假设需要旋转的节点为 x,其父亲为 y,以右旋为例)
将 y 的左儿子指向 x 的右儿子,且 x 的右儿子(如果 x 有右儿子的话)的父亲指向 y;ch[y][0]=ch[x][1]; fa[ch[x][1]]=y;
将 x 的右儿子指向 y,且 y 的父亲指向 x;ch[x][chk^1]=y; fa[y]=x;
如果原来的 y 还有父亲 z,那么把 z 的某个儿子(原来 y 所在的儿子位置)指向 x,且 x 的父亲指向 z。fa[x]=z; if(z) ch[z][y==ch[z][1]]=x;
Splay 操作
Splay 操作规定:每访问一个节点 x 后都要强制将其旋转到根节点。
Splay 操作即对 x 做一系列的 splay 步骤。每次对 x 做一次 splay 步骤,x 到根节点的距离都会更近。定义 p 为 x 的父节点。Splay 步骤有三种,具体分为六种情况:
zig: 在 p 是根节点时操作。Splay 树会根据 x 和 p 间的边旋转。zig 存在是用于处理奇偶校验问题,仅当 x 在 splay 操作开始时具有奇数深度时作为 splay 操作的最后一步执行。
splay-zig
即直接将 x 左旋或右旋(图 1, 2)
图 1
图 2
zig-zig: 在 p 不是根节点且 x 和 p 都是右侧子节点或都是左侧子节点时操作。下方例图显示了 x 和 p 都是左侧子节点时的情况。Splay 树首先按照连接 p 与其父节点 g 边旋转,然后按照连接 x 和 p 的边旋转。
splay-zig-zig
即首先将 g 左旋或右旋,然后将 x 右旋或左旋(图 3, 4)。
图 3
图 4
zig-zag: 在 p 不是根节点且 x 和 p 一个是右侧子节点一个是左侧子节点时操作。Splay 树首先按 p 和 x 之间的边旋转,然后按 x 和 g 新生成的结果边旋转。
splay-zig-zag
即将 x 先左旋再右旋、或先右旋再左旋(图 5, 6)。
图 5
图 6