[学习笔记] Splay & Treap 平衡树 - 数据结构
[学习笔记] Splay & Treap 平衡树 - 数据结构
Splay 树
又名伸展树,一种平衡二叉查找树,通过 \(\text{Splay}\) 操作不断把节点旋到根节点来维护整颗树的平衡。
说人话,很玄学的玩意,复杂度是单 log 级别的。为啥是单 log,科学的解释请移步 OI-WIKI。不科学的解释就是,通过不断 \(\text{Splay}\),以至于整棵树不会变成一条链,再加上二叉搜索树的看家本领,有点类似于二分查找,于是复杂度就变成 \(\mathcal{O}(n\log n)\) 了。
结构体定义
因为 Splay树 本质就是棵二叉查找树,所以维护的东西和二叉树一样。
struct{
int fa, ch[2], siz, cnt, val, tag;
};
int tot, rt;
fa
: 表示当前节点的父亲节点编号。ch[2]
: 表示当前节点的左右儿子编号。特殊地,左儿子的权值一定小于当前节点的权值,右儿子的权值一定大于当前节点的权值。siz
: 表示当前节点的子树大小。用于找第 \(k\) 最值。cnt
: 表示有多少和当前节点权值一样的节点。就是把权值一样的点并到一起。val
: 当前节点的权值。tot
: 记录点的编号。类似于动态开点,rt
: Splay树的根节点标号。tag
: 如果涉及区间修改,那么还要维护懒标记。
基本操作
inline bool get(int x){
return x == t[t[x].fa].ch[1];
}
用于找到该节点是父节点的哪一个儿子
inline void update(int x){
t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + t[x].cnt;
}
更新(统计)子树大小。类比于线段树的 upshup
操作,也可以维护区间和之类的东西。
Splay 操作
单次 splay 的本质就是,把某个节点与它父亲的关系调换一下。因为这是个二叉查找树,所以这种操作很方便:看图:
![image](https://img2024.cnblogs.com/blog/3358223/202408/3358223-20240829103247387-231048804.png)
假设 \(fa\) 的权值比 \(u\) 大,那么旋转过来之后 \(fa\) 就要变成 \(u\) 的右儿子。总的来说,就是父子关系调换,左右儿子关系调换。这是最基本的操作。
如果加上祖先节点,那么情况会有所不同:
通俗来讲,就是当父亲和祖先节点不共线的时候,先旋转 \(x\),再旋转 \(y\)。当父亲和祖先结点共线的时候,先旋转 \(y\),再旋转 \(x\)。有个口诀:折转底,直转中。
为什么要这么操作呢?如果不这么做会大幅增加你的时间复杂度,就是会 TLE。因为这么做可以最大限度度地保证树的平衡。
定义 splay(x, king)
表示把 \(x\) 节点旋转到 \(king\) 的儿子节点。特殊地,当 \(king=0\) 的时候,相当于把 \(x\) 旋转为根。
inline void splay(int x, int king){ //x 要旋转的节点 king 目标节点
while(t[x].fa != king){
int y = t[x].fa, z = t[y].fa;
// 判断2 3情况
if(z != king) (get(x) ^ getr(y)) ? rotate(x) : rotate(y);
// 无论哪一种情况,都要旋 x
rotate(x);
}
// 如果king == 0 那么就代表把 x 旋为根节点
if(king == 0) rt = x;
}
rotate 操作
rotate(x)
表示将 \(x\) 向上旋转一次。对于初学者来说,这个是最不好写的操作了。如果你忘了,不妨拿张演草纸手玩一下,按照下面的图,从上到下依次修改即可保证正确。注意一些节点的父子关系会发生变化。
inline void rotate(int x){
int y = t[x].fa, z = t[y].fa;
bool k = get(x);
t[z].ch[get(y)] = x;
t[x].fa = z;
t[t[x].ch[k ^ 1]].fa = y;
t[y].ch[k] = t[x].ch[k ^ 1];
t[x].ch[k ^ 1] = y;
t[y].fa = x;
update(y), update(x); //先更新儿子
}
插入操作
先按照二叉搜索树的方式去找这个节点,如果找不到,那么新开节点,如果这个节点本身就存在,那么 \(cnt+1\) 即可。最后都要把这个节点旋到根。
inline void insert(int x){
int u = rt, fa = 0;
while(u && x != t[u].val)
fa = u, u = t[u].ch[x > t[u].val];
if(u) ++t[u].cnt;
else{
u = ++cnt;
if(fa) t[fa].ch[x > t[fa].val] = u;
t[u].val = x, t[u].fa = fa, t[u].siz = t[y].cnt = 1;
} splay(u, 0);
}
find 操作
找到权值 \(x\) 在树里对应的编号,并将它旋转到根。如果不存在,那么旋转与它权值最为相近的节点。
inline void find(x){
int u = rt;
while(t[u].ch[x > t[u].val] && x != t[u].val)
u = t[u].ch[u > t[u].val];
splay(u, 0);
}
删除操作
找到权值在