快速入门Splay
\(splay\) :伸展树(\(Splay Tree\)),也叫分裂树,是一种二叉排序树,它能在\(O(log n)\)内完成插入、查找和删除操作。它由\(Daniel Sleator\)和\(Robert Tarjan\)创造,后勃刚对其进行了改进。它的优势在于不需要记录用于平衡树的冗余信息。在伸展树上的一般操作都基于伸展操作。
先让我们看一下一棵二叉搜索树(\(Binary\) \(Search\) \(Tree\))是什么样子的。

如图所示,对任意一棵\(BST\),它有以下性质:
- 是一棵空树,或者是具有下列性质的二叉树
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
根据定义,我们会发现:
- 这棵树的中序遍历,与其压成数组的升序排序等效
在这样一棵树中,我们可以很容易地维护以下信息:
- 查询\(x\)数的排名
- 查询排名为\(x\)的数
- 求\(x\)的前驱(前驱定义为小于\(x\),且最大的数)
- 求\(x\)的后继(后继定义为大于\(x\),且最小的数)
同样的我们会发现,对于一个固定的数列,它可以形成很多种不同类型的\(BST\)。如果这棵树恰好不太优美,每次维护的复杂度可能会被卡到\(O(N)\)(一条链)。
所以,平衡树这种伟大的数据结构就诞生啦!
顾名思义,平衡树就是一棵可以保持全树平衡的二叉搜索树,以此避免复杂度退化为\(O(N)\)。比较经典的一种平衡树是\(Treap\),它基于的是对一个有序数列,随机出的\(BST\)期望复杂度是\(O(logN)\),通过利用堆的性质来维护其随机性,这个东西我的上一篇博客已经介绍过,不再展开介绍。今天我们要介绍的是另一种经典的平衡树——\(Splay\)。
既然是平衡树,\(Splay\)是如何实现其树体平衡的呢?
在\(Splay\)的每一个维护操作中,维护结束后当前被维护的点都会被旋转成为树的根节点,这个过程叫做树的伸展。\((Splay)\)。伸展是\(Splay\)的核心操作。与\(Treap\)利用随机出来的优先级进行堆的维护不同,\(Splay\)的大多数操作都要基于伸展操作,这也决定了\(Splay\)相比前者具有更广泛的适用性。
那\(Splay\)是怎么保证其复杂度不退化成\(O(N)\)的呢?来看个例子。

在这一棵已经退化成链的\(BST\)中,我们对最底下那个节点进行了一次维护。在这之后,这个节点就开始了向根节点的漫漫伸展之路~
所以在伸展过程结束后,这棵树就再次自发地进化回了一棵正常的树。如果深度更深会更加明显,在一次\(Splay\)以后,它会从\(N\)级别的深度进化为\(logN\)级别。
接着让我们贪心地想一想,假如现在这棵树非常的不优秀。我想要把它卡掉,就应该总是访问它最不优秀的节点。如果最开始它还有很多超级长的链,那么经过几次贪心的访问之后,它的所有链中的最大深度就已经回到\(logN\)了。不管常数怎么样,均摊一下复杂度是没有问题了。
既然这些操作\(Treap\)也能做,为什么不用又快又好写的\(Treap\)呢?因为\(Splay\)在区间操作和\(LCT\)中有其不可替代的作用。具体是什么作用,我也没有学到,等到学了在拿出来讲吧QwQ
讲过了原理,我们可以来看一下代码实现了Qw
inline void push_up (int u) {
t[u].sz = t[u].cnt;
t[u].sz += t[t[u].ch[0]].sz;
t[u].sz += t[t[u].ch[1]].sz;
}
inline void rotate (int x) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
connect (z, x, d2);
connect (y, t[x].ch[!d1], d1);
connect (x, y , !d1);
push_up (y);
push_up (x);
}
这里\(connect\)是一个连边的函数,旋转的原理和\(Treap\)一样,都是要保证其\(BST\)的性质,可以手画一下示意图就明白啦~
inline void splay (int x, int goal) {
if (x == 0) return;
while (t[x].fa != goal) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
if (z != goal) {
if (d1 == d2) {
rotate (y);
} else {
rotate (x);
}
}
rotate (x);
}
if (goal == 0) {
root = x;
}
}
核心操作——伸展,可以思考一下:为什么是把\(x\)旋转为\(goal\)的子节点?
剩下的操作,作者很懒,就只贴上代码啦~
inline void find (int key) {
int u = root;
if (u == 0) return;
while (t[u].key != key && t[u].ch[key > t[u].key]) {
u = t[u].ch[key > t[u].key];
}
//找到key对应的节点,并把它旋转到根。
splay (u, 0);
}
inline void Insert (int key) {
int u = root, fa = 0;
while (u != 0 && t[u].key != key) {
fa = u;//记得记录父亲
if (key > t[u].key) {
u = t[u].ch[1];
} else {
u = t[u].ch[0];
}
}
if (u != 0) {
//已有(能查到)
++t[u].sz;
++t[u].cnt;
} else {
//新增
u = ++max_size;
t[u].sz = 1;
t[u].cnt = 1;
t[u].key = key;
connect (fa, u, key > t[fa].key);
}
splay (u, 0);
}
inline int Next (int key, int dir) {
//dir = 0 -> 前驱
//dir = 1 -> 后继
find (key);
int u = root;
if (dir == 0 && t[u].key < key) return u;
if (dir == 1 && t[u].key > key) return u;
//如果key值并没有存在于树中:
u = t[u].ch[dir];
while (t[u].ch[!dir]) {
u = t[u].ch[!dir];
}
//e.g 如果要找前驱,就先往左一步(保证一定比当前值更小),再一直向右(最大的那个)。
return u;
}
inline void Delete (int key) {
int _pre = Next (key, 0);
int _nxt = Next (key, 1);
splay (_pre, 0000);
splay (_nxt, _pre);
//当前键值key的前驱是_pre, 后继是_nxt
//_pre被旋转到根节点,_nxt成为_pre的子节点(显然是右)
//那么当前点一定在_nxt的左边,而且底下没有任何一个点。
int u = t[_nxt].ch[0];
if (t[u].cnt > 1) {
--t[u].cnt;
splay (u, 0);
} else {
t[_nxt].ch[0] = 0;
}
}
inline int kth (int k) {
int u = root;
if (u == 0) return 0;
while (u != 0) {
int ls = t[u].ch[0];
int rs = t[u].ch[1];
if (k > t[ls].sz + t[u].cnt) {
k -= t[ls].sz + t[u].cnt;
u = rs;//格外注意不要写反顺序
} else if (k <= t[ls].sz) {
u = ls;
} else {
return t[u].key;
}
}
return false;
}
inline int get_rnk (int key) {
find (key);
return t[t[root].ch[0]].sz;
}
还有一点需要注意的,\(splay\)在使用前要先\(insert\)一个极大值和一个极小值。否则在\(Next\)函数的查找中,比如只有一个点的话,会出现找不到前驱和后继的情况,也就会导致出莫名其妙的锅。当然,加上极大极小值之后要格外注意对答案的处理。下面给出完整代码,题目P3369 【模板】普通平衡树。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100010
#define INF 0x7fffffff
using namespace std;
struct Splay_Tree {
int root, max_size;
struct Splay_Node {
int sz, fa, cnt, key, ch[2];
}t[N];
Splay_Tree () {
root = max_size = 0;
memset (t, 0, sizeof (t));
}
inline void connect (int u, int v, int dir) {
t[u].ch[dir] = v;
t[v].fa = u;
}
inline void push_up (int u) {
t[u].sz = t[u].cnt;
t[u].sz += t[t[u].ch[0]].sz;
t[u].sz += t[t[u].ch[1]].sz;
}
inline void rotate (int x) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
connect (z, x, d2);
connect (y, t[x].ch[!d1], d1);
connect (x, y , !d1);
push_up (y);
push_up (x);
}
inline void splay (int x, int goal) {
if (x == 0) return;
while (t[x].fa != goal) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
if (z != goal) {
if (d1 == d2) {
rotate (y);
} else {
rotate (x);
}
}
rotate (x);
}
if (goal == 0) {
root = x;
}
}
inline void find (int key) {
int u = root;
if (u == 0) return;
while (t[u].key != key && t[u].ch[key > t[u].key]) {
u = t[u].ch[key > t[u].key];
}
splay (u, 0);
}
inline void Insert (int key) {
int u = root, fa = 0;
while (u != 0 && t[u].key != key) {
fa = u;
if (key > t[u].key) {
u = t[u].ch[1];
} else {
u = t[u].ch[0];
}
}
if (u != 0) {
++t[u].sz;
++t[u].cnt;
} else {
u = ++max_size;
t[u].sz = 1;
t[u].cnt = 1;
t[u].key = key;
connect (fa, u, key > t[fa].key);
}
splay (u, 0);
}
inline int Next (int key, int dir) {
find (key);
int u = root;
if (dir == 0 && t[u].key < key) return u;
if (dir == 1 && t[u].key > key) return u;
u = t[u].ch[dir];
while (t[u].ch[!dir]) {
u = t[u].ch[!dir];
}
return u;
}
inline void Delete (int key) {
int _pre = Next (key, 0);
int _nxt = Next (key, 1);
splay (_pre, 0000);
splay (_nxt, _pre);
int u = t[_nxt].ch[0];
if (t[u].cnt > 1) {
--t[u].cnt;
splay (u, 0);
} else {
t[_nxt].ch[0] = 0;
}
}
inline int kth (int k) {
int u = root;
if (u == 0) return 0;
while (u != 0) {
int ls = t[u].ch[0];
int rs = t[u].ch[1];
if (k > t[ls].sz + t[u].cnt) {
k -= t[ls].sz + t[u].cnt;
u = rs;//格外注意
} else if (k <= t[ls].sz) {
u = ls;
} else {
return t[u].key;
}
}
return false;
}
inline int get_rnk (int key) {
find (key);
return t[t[root].ch[0]].sz;
}
}st;
int n, x, opt;
int main () {
// freopen ("splay.in", "r", stdin);
scanf ("%d", &n);
st.Insert (+INF);
st.Insert (-INF);
for (int i = 1; i <= n; ++i) {
scanf ("%d %d", &opt, &x);
if (opt == 1) {
st.Insert (x);
}
if (opt == 2) {
st.Delete (x);
}
if (opt == 3) {
printf ("%d\n", st.get_rnk (x));
}
if (opt == 4) {
printf ("%d\n", st.kth (x + 1));
}
if (opt == 5) {
printf ("%d\n", st.t[st.Next (x, 0)].key);
}
if (opt == 6) {
printf ("%d\n", st.t[st.Next (x, 1)].key);
}
}
}
快速入门Splay的更多相关文章
- Web Api 入门实战 (快速入门+工具使用+不依赖IIS)
平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...
- SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=》提升)
SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=>提升,5个Demo贯彻全篇,感兴趣的玩才是真的学) 官方demo:http://www.asp.net/si ...
- 前端开发小白必学技能—非关系数据库又像关系数据库的MongoDB快速入门命令(2)
今天给大家道个歉,没有及时更新MongoDB快速入门的下篇,最近有点小忙,在此向博友们致歉.下面我将简单地说一下mongdb的一些基本命令以及我们日常开发过程中的一些问题.mongodb可以为我们提供 ...
- 【第三篇】ASP.NET MVC快速入门之安全策略(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- 【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- Mybatis框架 的快速入门
MyBatis 简介 什么是 MyBatis? MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架.MyBatis 消除 了几乎所有的 JDBC 代码和参数的手工设置以及结果 ...
- grunt快速入门
快速入门 Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. Grunt 0.4.x 必须配合Node.js >= 0.8.0版本使用.:奇数版本 ...
- 【第一篇】ASP.NET MVC快速入门之数据库操作(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- 【第四篇】ASP.NET MVC快速入门之完整示例(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
随机推荐
- Python——进程队列
队列 先进先出 from multiprocessing import Queue q = Queue(5) #队列的大小 q.put(1) #放入内容 q.put(2) #放入内容 q.put(3) ...
- Nginx 减少关闭连接的time_wait端口数量
L:129
- HTC Vive 基础入门 基于Unreal Engine 4引擎
主要以讲解介绍HTC Vive设备以及Unreal继承的Steam VR Plugin为主 使用最新的虚幻引擎与Plugin完成VR环境的搭建 然后完成一个基本的VR Games. 任务5: 04-配 ...
- [Codeforces1132G]Greedy Subsequences——线段树+单调栈
题目链接: Codeforces1132G 题目大意:给定一个序列$a$,定义它的最长贪心严格上升子序列为$b$满足若$a_{i}$在$b$中则$a_{i}$之后第一个比它大的也在$b$中.给出一个数 ...
- SHELL编程综合练习
SHELL编程综合练习1 一. 实验准备(预防抄袭,此步必做) 请将命令提示符设为你的学号:输入PS1=你的学号_,即学号之后跟下划线,例如PS1=110015_, 回车执行 如发现命令提示符和你的学 ...
- Log Parser Studio 分析 IIS 日志
Log Parser Studio 分析 IIS 日志 来源 https://www.cnblogs.com/lonelyxmas/p/8671336.html 软件下载地址: Log Parser ...
- 使用kubeadm安装Kubernetes
Docker安装 yum install -y yum-utils yum-config-manager --add-repo https://docs.docker.com/v1.13/engine ...
- UOJ #207. 共价大爷游长沙(LCT + 异或哈希)
题目 维护一颗动态树,并维护一个点对集合 \(S\) . 动态查询一条边,是否被集合中所有点对构成的路径包含. \(n \le 100000, m \le 300000\) 题解 orz 前辈 毛爷爷 ...
- module 'sign.views' has no attribute 'search_name'
百度找到如下链接 http://lovesoo.org/python-script-error-attributeerror-module-object-has-no-attribute-solve- ...
- Nginx-Cluster 构建
nx-Cluster and ReverseProxyServer-----------ReProxy-------------------------Client-----------192.168 ...