\(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的更多相关文章

  1. Web Api 入门实战 (快速入门+工具使用+不依赖IIS)

    平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...

  2. SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=》提升)

     SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=>提升,5个Demo贯彻全篇,感兴趣的玩才是真的学) 官方demo:http://www.asp.net/si ...

  3. 前端开发小白必学技能—非关系数据库又像关系数据库的MongoDB快速入门命令(2)

    今天给大家道个歉,没有及时更新MongoDB快速入门的下篇,最近有点小忙,在此向博友们致歉.下面我将简单地说一下mongdb的一些基本命令以及我们日常开发过程中的一些问题.mongodb可以为我们提供 ...

  4. 【第三篇】ASP.NET MVC快速入门之安全策略(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  5. 【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  6. Mybatis框架 的快速入门

    MyBatis 简介 什么是 MyBatis? MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架.MyBatis 消除 了几乎所有的 JDBC 代码和参数的手工设置以及结果 ...

  7. grunt快速入门

    快速入门 Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. Grunt 0.4.x 必须配合Node.js >= 0.8.0版本使用.:奇数版本 ...

  8. 【第一篇】ASP.NET MVC快速入门之数据库操作(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  9. 【第四篇】ASP.NET MVC快速入门之完整示例(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

随机推荐

  1. HTTP协议 - 使用php模拟get/post请求

    首先 有个疑问, 是不是只有浏览器才能发送http 请求? 答案肯定是错的,第一篇就说了,http是由请求行,请求头,请求主体三个部分组成,那么我们可不可以用代码来模拟一下get和post请求呢: 首 ...

  2. 【python练习题】程序14

    #题目:将一个正整数分解质因数.例如:输入90,打印出90=2*3*3*5. #我的方法应该比网上的更加简洁,只是递归可能速度慢 n = input('请输入一个正整数:') n = int(n) X ...

  3. 配置 BizTalk Server

    使用“基本配置”或“自定义配置”配置 BizTalk Server. 基本配置与自定义配置       如果配置使用域组,则进行“自定义配置”. 如果配置使用自定义组名称而不是默认组名称,则进行“自定 ...

  4. SharePoint 2013 使用 RBS 功能将二进制大型对象 BLOB 存储在内容数据库外部。

    为每个内容数据库设置 BLOB 存储   启用并配置 FILESTREAM 之后,请按照以下过程在文件系统中设置 BLOB 存储.必须为要对其使用 RBS 的每个内容数据库设置 BLOB 存储. 设置 ...

  5. 【NLP】How to Generate Embeddings?

    How to represent words. 0 . Native represtation: one-hot vectors Demision: |all words| (too large an ...

  6. CentOS安装、配置Nginx反向代理

    添加Nginx存储库 sudo yum install epel-release 安装Nginx sudo yum install nginx 启动Nginx sudo systemctl start ...

  7. Asteroids POJ - 3041 匈牙利算法+最小点覆盖König定理

    题意: 给出一个N*N的地图N   地图里面有K个障碍     你每次可以选择一条直线 消除这条直线上的所有障碍  (直线只能和列和行平行) 问最少要消除几次 题解: 如果(x,y)上有一个障碍 则把 ...

  8. BZOJ1012 最大数maxnumber

    单调栈的妙处!! 刚看到这题差点写个splay..但是后来看到询问范围的只是后L个数,因为当有一个数新进来且大于之前的数时,那之前的数全都没有用了,满足这种性质的序列可用单调栈维护 栈维护下标(因为要 ...

  9. bzoj 1257: [CQOI2007]余数之和 (数学+分块)

    Description 给出正整数n和k,计算j(n, k)=k mod 1 + k mod 2 + k mod 3 + … + k mod n的值 其中k mod i表示k除以i的余数. 例如j(5 ...

  10. Jdk1.6编译,1.7执行,1.7中没有需要的类,为何不会报错

      今天发现一个非常奇怪的问题   import sun.misc.BASE64Decoder; 我在本地引入了jdk1.6中的包,编译也用1.6没问题,但是实际上jdk1.7并没有这个包.在Linu ...