Splay
Splay 是一个基于 BST (Binary Search Tree 二叉搜索树) 的动态数据结构。
Splay 所操作的对象必须要有 $key$ 且 $key$ 严格弱序(可以计算排名)。
它支持以均摊单次 $\mathcal O(\log n)$ 的时间复杂度进行如下操作:
insert(x)插入一个元素,以 $x$ 为 $key$。erase(x)删除 $key=x$ 的元素。find(x)查找值为 $x$ 的元素的信息。at(k)查找排名第 $k$ 的元素的信息。range(l, r)提取出排名在 $l \sim r$ 的区间merge(tr)将另外一颗 splay 树拼接到这颗 splay 树上。
要求:若要将 $Tb$merge到 $Ta$,必须满足 $\max\{i : i \in Tb\} \le \min\{j : j \in Ta\}$ 或者 $\min\{i : i \in Tb\} \ge \max\{j : j \in Ta\}$
性质
假设现在 Splay 维护了一个有序的序列,以 $key$ 为关键字。
这里,给出 Splay 的几条性质:
- Splay 树上的每一个节点都代表原序列中的一段区间。
- Splay 树上的每一颗子树都是 Splay 树。
- Splay 树的中序遍历就是排好序的原序列。
- Splay 树支持缓存,即访问越频繁,访问速度越快。
- Splay 不支持可持久化。
算法流程
节点构造
我们使用一个三元组 $(size, key, ch_0, ch_1, fa, dat)$ 来表示 Splay 树上的一个节点。
- $size$:当前子树内元素个数。
- $key$:当前节点对应元素的键值。
- $ch_{0/1}$:左/右孩子。
- $fa$:父亲。
- $dat$:节点的附加信息。
建树
如果我们有一个已经有序的序列 $a_{1…n} = (key_1, dat_1), (key_2, dat_2), \dots, (key_n, dat_n)$,
那我们直接根据 BST 的性质建树即可,即建一颗只有右孩子的二叉树。
- 通常来说,为了方便之后的操作,我们会设置两个哨兵节点,一首一尾。
graph TD
A{Root} --> B[NULL]; A --> C(1);
C --> D[NULL]; C --> E(2);
E --> F[NULL]; E --> G(3);
G --> H[NULL]; G --> I{Guard}
维护
在节点上
分为 pushup 和 pushdown,其中 pushup 需要统计 $size$ 即 $size := size_0 + 1 + size_1$。
在树的结构上
分为 zig、zig-zig、zig-zag。
flowchart LR
subgraph Before[ ]
direction TB
f0((anc)) <--> y0((y))
y0 <--> x0((x))
x0 <--> a0(A)
x0 <--> b0(B)
y0 <--> c0(C)
end
subgraph After[ ]
direction TB
f1((anc)) <--> x1((x))
x1 <--> a1(A)
x1 <--> y1((y))
y1 <--> b1(B)
y1 <--> c1(C)
end
Before -->|右旋| After
Before <---|左旋| After
linkStyle 0,1,3,10 stroke:red,stroke-width:2px
linkStyle 5,7,8,11 stroke:cyan,stroke-width:2px
flowchart LR
subgraph Beg1[ ]
direction TB
x0((x)) --> y0((y)) --> z0((z)) --> c0(C)
x0 --> a0(A)
y0 --> b0(B)
z0 --> d0(D)
end
subgraph Mid1[ ]
direction TB
y1((y)) --> x1((x)) --> a1(A)
x1 --> b1(B)
y1 --> z1((z)) --> c1(C)
z1 --> d1((D))
end
subgraph End1[ ]
direction TB
z2((z)) --> a2(A)
z2 --> y2((y)) --> b2(B)
y2 --> x2((x)) --> c2(C)
x2 --> d2(D)
end
Beg1 -->|右旋| Mid1 -->|右旋| End1
Beg1 <---|左旋| Mid1 <---|左旋| End1
linkStyle 18,19 stroke:red,stroke-width:2px
linkStyle 20,21 stroke:green,stroke-width:2px
style y0 fill:red
style x1 fill:red
style z1 fill:green
style y2 fill:green
flowchart LR
subgraph Beg[ ]
direction TB;
x0((x)) --> y0((y)) --> a0(A); y0 --> z0((z)) --> b0(B); z0 --> c0(C); x0 --> d0(D);
end
subgraph Mid[ ]
direction TB;
x1((x)) --> z1((z)) --> y1((y)) --> a1(A); y1 --> b1(B); z1 --> c1(C); x1 --> d1(D);
end
subgraph End[ ]
direction TB;
z2((z)) --> x2((x)) --> a2(A); x2 --> b2(B); z2 --> y2((y)) --> c2(C); y2 --> d2(D);
end
subgraph iMid[ ]
direction TB;
x3((x)) --> a3(A); x3 --> z3((z)) --> b3(B); z3 --> y3((y)) --> c3(C); y3 --> d3(C);
end
subgraph iBeg[ ]
direction TB;
x4((x)) --> a4(A); x4 --> y4((y)) --> z4((z)) --> b4(B); z4 --> c4(C); y4 --> d4(D);
end
Beg -->|右旋| Mid -->|右旋| End
iBeg -->|左旋| iMid -->|左旋| End
linkStyle 30,31 stroke:red,stroke-width:2px
linkStyle 32,33 stroke:green,stroke-width:2px
style z0 fill:red
style z1 fill:red
style z3 fill:green
style z4 fill:green
如你所见,我们这么大费周折地进行旋转,无非就是为了在保证 BST 的性质下尽可能地降低当前节点的深度。
所以只要我们一直做 zig-zig 或者 zig-zag,直到不行为止。最后判断要不要补一个 zig 就可以将给定节点旋转到根了。
其实 rotate 关键就是重建 3 条边(有的时候因为空节点没那么多),注意双向($ch,fa$),记牢了不容易忘。
1 | struct Node { |
查询
$\tt{find(x)}$
令 $\text{value}_{0/1}$ 表示左/右孩子的 $\text{value}$,
直接使用类似于二分的策略,当前节点 $\begin{cases}
size_0 + 1 = x && \text{Ret} & \texttt{this} \\
x \le size_0 && \text{Ret} & \texttt{find(x)} \text{ of } ch_0 \\
x \gt size_0 + 1 && \text{Ret} & \texttt{find(x} - sz_0 - 1\texttt{)} \text{ of } ch_1
\end{cases}$。所以还是很简单的。
注意,为了确保正确的时间复杂度和 Splay 的缓存机制,请你必须将查询节点旋转到根。
$\tt{at(x)}$
这个就是直接 BST 上搜索就可以了。如果 $x \le key$ 就返回 $ch_0.\tt{at(x)}$,否则就是 $ch_1.\tt{at(x)}$。如果发现 $ch_{0/1}$ 是空的那就结束。
注意最后旋转到根。
重点注意
- 需要在访问节点的 $ch_?$ 的时候进行
pushdown,不要忘记。
修改
先调用 $\tt{find(x)}$,对他的 $dat$ 进行修改,然后旋转到根。
合并
注意到我们将排名为 $size$ 的节点旋转到根后,右子树为空,直接把另外一颗 Splay 树接上去就可以了(反之亦然)。