建议先完成 让刚访问的点靠近你,再用 把一段话翻过来 预演双哨兵和 Lazy。前者从旋转的中序证据出发,后者把区间真的夹成一棵子树。
理解 Splay 通过 zig/zig-zig/zig-zag 把访问点伸到目标父亲,掌握按值有序集与按排名序列两种模式,并用双哨兵把任意区间暴露为一棵子树。
前置:BST 旋转、子树大小、Lazy 翻转。
Splay 不维护颜色/随机优先级。每次访问后把目标伸到根或指定父亲:
它保持 BST 中序不变,并把近期访问点移近根。Sleator–Tarjan 势能分析给出摊还对数,但不能声称每次最坏对数。
隐式序列中加入左右哨兵。要暴露原区间 $[l,r]$:把前驱(全树第 l)伸到根,再把后继(全树第 r+2)伸成前驱右儿子;后继的左子树恰是目标区间。
BST 中序即升序。countLess(x) 走树:若当前键小于 x,则左子树和当前点全部计入并转右,否则转左。kth 用左 size 决策。插删保持 BST 顺序,旋转只改变形状不改变中序,因此四种操作语义正确;Splay 定理给出操作序列摊还 $O(\log n)$。
splay(x, goal):
push all ancestors
while parent(x) != goal:
if parent(parent(x)) == goal: rotate(x)
else if x and parent(x) have same direction: rotate(parent(x)); rotate(x)
else: rotate(x); rotate(x)
选择区间前驱 x 与后继 y。中序中所有目标元素严格位于 x,y 之间;把 x 伸根、y 伸到 x 下后,若 y 左子树缺或多元素就会违反 BST 中序连续性,因此它恰是目标区间。读取 sum 或应用 rev 后,pull y、x 恢复摘要。每操作摊还 $O(\log n)$。
x = kth(l) # left sentinel included
splay(x, root)
y = kth(r + 2)
splay(y, parent=x)
segment = left_child(y)
reverse: apply_rev(segment)
sum: segment.sum
HDU-3487 Play with Chain 和 POJ-3580 SuperMemo 可继续训练剪贴、循环移位、区间加与最小值。FHQ 更易写分裂合并,Splay 更擅长围绕访问位置暴露区间;应按操作接口选择。
先只实现有序集并与 std::set 对拍,再加隐式序列。调试区间暴露时打印中序排名、x、y 和目标子树长度,优先排查哨兵偏移。