W
e
l
c
o
m
e
: )

[学习笔记] 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

假设 \(fa\) 的权值比 \(u\) 大,那么旋转过来之后 \(fa\) 就要变成 \(u\) 的右儿子。总的来说,就是父子关系调换,左右儿子关系调换。这是最基本的操作。

如果加上祖先节点,那么情况会有所不同:

image

通俗来讲,就是当父亲和祖先节点不共线的时候,先旋转 \(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\) 向上旋转一次。对于初学者来说,这个是最不好写的操作了。如果你忘了,不妨拿张演草纸手玩一下,按照下面的图,从上到下依次修改即可保证正确。注意一些节点的父子关系会发生变化。

image

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);
}

删除操作

找到权值在

posted @ 2024-08-29 09:43  XiaoLe_MC  阅读(2)  评论(0编辑  收藏  举报