【C++进阶】带你打穿高级数据结构天花板:Splay、K-D树与LCT动态树
导语:
在算法竞赛(ACM/ICPC)中,普通的二叉搜索树、线段树已经无法满足考官的胃口了。如果遇到频繁的区间翻转、多维空间的最近点查找,甚至是树的边不断断开又连上的变态场景,你该怎么办?今天,博主带你一次性拿下算法竞赛界的三座大山:Splay树(伸展树)、K-D Tree(K维树)和 LCT(Link Cut Tree 动态树)。
别怕,再难的算法也能拆解成搭积木!建议先收藏,再慢慢啃,这绝对是你能在全网找到的最清晰的通俗解密!
一、Splay树:极其灵活的“区间杂技大师”
很多同学学过Treap或AVL树,为了维持平衡,它们需要复杂的左右旋和高度计算。而 Splay(伸展树)的核心思想非常粗暴且有效:“刚刚被访问过的数据,极有可能再次被访问(局部性原理)。”
所以,Splay 不记录任何优先权或高度,它的唯一法则就是:不管访问了谁,都通过一系列旋转,把它一路“提根”(转到树的根节点)。
1. 核心魔法:双旋(Zig-Zig 与 Zig-Zag)
Splay 的平摊时间复杂度是优雅的 O(logn)O(\log n)O(logn)。但如果只是简单地一层一层单旋,遇到链状树就会退化。Splay 的灵魂在于**“双旋”**。
设当前节点为 xxx,父节点为 fff,祖父节点为 ggg。
- 一字旋 (Zig-Zig):x,f,gx, f, gx,f,g 在一条直线上(比如都是左儿子)。
核心口诀:先转爹,再转娃! 先旋转 fff,再旋转 xxx。这能极大地减少树的层数。 - 之字旋 (Zig-Zag):x,f,gx, f, gx,f,g 不在一条直线上(比如一个是左儿子,一个是右儿子)。
核心口诀:直接连转两次娃! 连续两次旋转 xxx。
2. Splay 的杀手锏:区间操作
为什么线段树能做的事,很多还要用 Splay?因为 Splay 特别适合做区间的物理切分与合并!
假设我们要提取区间 [L,R][L, R][L,R] 进行操作(比如翻转):
- 把排名第 L−1L-1L−1 的节点 Splay 到根。
- 把排名第 R+1R+1R+1 的节点 Splay 到根的右儿子。
- 此时,根的右儿子的左子树,就是完完整整的区间 [L,R][L, R][L,R]!
你可以直接把这棵子树切下来,或者打上 Lazy Tag,简直不要太爽!
** Splay 核心旋转逻辑解析:**
// 核心逻辑:将x旋转到goal的儿子位置
void splay(int x, int goal) {
if (goal == 0) root = x;
while (1) {
int f = t[x].fa, g = t[f].fa;
if (f == goal) break;
if (g != goal) {
// 如果有祖父节点,判断是一字旋还是之字旋
if (get(x) == get(f)) rotate(f); // 同向(一字旋):先转父节点
else rotate(x); // 异向(之字旋):先转自己
}
rotate(x); // 最后转自己
}
Update(x);
}
二、K-D Tree:降维打击,多维空间的“导航仪”
现在,题目不在一维的数轴上了,而是给出了平面上的 10510^5105 个坐标点,每次询问距离点 (x,y)(x, y)(x,y) 最近的点是谁。
暴力做是 O(n2)O(n^2)O(n2),直接 TLE 飞起。这时候,K-D Tree (K-Dimensional Tree) 闪亮登场。
1. 核心思想:几何+二分
一维我们用二分法,二维/三维怎么办?轮流切蛋糕!
- 第一层:按 xxx 坐标找中位数,把平面劈成左右两半;
- 第二层:按 yyy 坐标找中位数,把平面劈成上下两半;
- 第三层:再按 xxx 切… 如此交替。
建树后,每个节点不仅是一个点,更代表了平面上的一个矩形区域。
2. 剪枝的艺术:最近邻搜索 (NN)
在 K-D Tree 上找最近点,核心在于**“画圆”与“矩形求交”**。
- 我们从根节点往下找,先假定一个答案(比如当前距离 rrr)。
- 以目标点为圆心,rrr 为半径画一个圆。
- 如果目标点在左子树,我们先去左子树搜。
- 高能预警(剪枝核心):搜完左子树后,我们要不要去右子树?就看这个圆有没有和右子树代表的“矩形区域”相交! 如果没相交,说明右侧绝对不可能有更近的点,直接放弃右子树(剪枝)!
这种算法能把 O(n)O(n)O(n) 的查询硬生生降到 O(logn)O(\log n)O(logn) 或 O(n)O(\sqrt{n})O(n)级别!
(注:如果频繁插入删除导致树不平衡,通常引入替罪羊树的思想:发现某棵子树太胖了,直接把这棵子树“拍平”,重新建树!)
三、LCT 动态树:数据结构界的“最终Boss”
如果说前面两个只是难,那 LCT (Link Cut Tree) 就是真·天花板。
普通的树结构是静态的,但 LCT 要解决的问题是:树的节点在运行过程中,会不断地断开连线(Cut),又不断地重新连线(Link),并且还要查询两点之间的路径信息!
LCT 巧妙地融合了 树链剖分 和 Splay树。
1. 实虚交替的“平行宇宙”
LCT 中有两棵树:原树(题目真实的树)和 辅助树(由无数个 Splay 树组成)。
- 原树的边分两种:
- 实边:每个节点最多只能有一条实边连向儿子。实边连成的路径叫“实链”。
- 虚边:其余的边。认父不认子(儿子知道父亲是谁,但父亲的 Splay 儿子指针里没有它)。
- 辅助树的规则:
- 原树中的每一条“实链”,都用一棵 Splay 树来维护。
- Splay 树的中序遍历结果,严格对应原树中深度从浅到深的节点!
2. LCT 的灵魂函数:access(x)
LCT 所有神级操作的基础,就是 access(x)。
它的作用是:在原树中,强行打通一条从根节点到节点 xxx 的实链(高速公路)。
access(x) 的精妙步骤:
- 沿着虚边不断向上爬。
- 每次把当前节点 Splay 到它所在的辅助树的根。
- 强行把它的右儿子(代表深度比它深的节点)换成上一步所在的链(变虚为实)。
- 更新信息。
3. 神奇的 makeroot(x)
很多题目没有固定的根,怎么办?makeroot(x) 能把 xxx 变成整棵原树的根!
步骤:
access(x):打通根到 xxx 的路。此时 xxx 在原树的实链底端(深度最深)。splay(x):把 xxx 转到 Splay 树的根。此时 xxx 没有右儿子。reverse(x):最骚的一步!给 xxx 打上 Splay 的翻转 Tag。原树中的深度关系瞬间倒转,xxx 变成了深度最浅的,即变成了根!
** LCT 的 Link 和 Cut 操作简直像艺术:**
// 连边操作:在 x 和 y 之间连一条边
void link(int x, int y) {
makeroot(x); // 把 x 变成原树的根
if (findroot(y) != x) // 如果它们不在同一棵树里
t[x].fa = y; // x 认 y 作父(连一条虚边)
}
// 断边操作:断开 x 和 y
void cut(int x, int y) {
makeroot(x); // 把 x 变成原树的根
access(y); // 打通 x 到 y 的实链
splay(y); // 把 y 转到辅助树的根
// 断绝父子关系
if (t[y].ls == x && !t[x].rs) {
t[y].ls = t[x].fa = 0;
pushup(y);
}
}
总结
- Splay:通过无休止的旋转实现自平衡,利用“双旋”保证性能,是动态区间操作的王。
- K-D Tree:巧妙融合空间分割与二分思想,配合剪枝大法,是多维坐标查询的利器。
- LCT 动态树:把静态的重链剖分动态化,用 Splay 维护实链,实虚边灵活切换,是动态图/森林连通性问题的终极武器。
当你能手撕这三个数据结构时,恭喜你,你的代码能力、对指针的理解、对递归与分治的掌握,已经超越了 95% 的同龄人!
** 博主有话说:**
这篇文章拆解这三大高级数据结构,画图、码字不易!如果你觉得这篇文章打通了你的任督二脉,求点赞、收藏、评论一键三连!你有在比赛或面试中被哪些数据结构坑过吗?欢迎在评论区吐槽交流,我们下期见!
更多推荐


所有评论(0)