普通Splay详解
预备知识:
至于BST,随便看一下就可以,
我们知道二叉搜索树是O(logN)的,那我们为什么要用平衡树呢?
之前我们了解到,BST的插入是小的往左子树走,大的往右子树走,如果凉心出题人给出的序列是有序的呢
这样我们就只能O(N)的操作,GG
旋转(rotate):
Splay的经典操作就是旋转
在Splay中,我们用旋转来保持平衡,也就是保持是log(N)的量级
旋转就是将节点向上旋转到父亲节点的位置,同时保持平衡
有zig,和zag两种情况(其实都一个样)
具体要怎么旋转呢
如图,X,Y,Z 是三个节点,A,B,C 是三颗子树,我们要把 Z 转到 Y 的位置
其实就只有3步
根据平衡树的性质
Z 是 Y 的左儿子,所以Z < Y
Y 是 X 的左儿子,所以Y < X
我们要把 Z 旋转上去的话,就把 Z 放到 Y 的位置
整完了长这样
因为我们还没操作 Y,所以Y还有连向其父亲和儿子的边
总结一下
Step1:把要旋转的节点放到父亲的位置
而 Y > Z 且 Y < X,所以这时Y就成了Z的右儿子
总结一下
Step2:把要旋转节点的父亲设为其儿子
这时会有三个节点(子树)连向 Z,而Y只有一个儿子,显然,Z,子树 B 和子树 C 都是小于 Y 的,子树 B 大于 Z,所以 B 成为 Y 的左儿子
这样就完成了
总结一下
Step3:把 父节点所占旋转节点的儿子 设为父节点的对应儿子
代码:
定义一波:
struct tree {
int fa, cnt, sum, val;
//父亲
//计数(几个值为x的点)
//以当前点为根节点的子树节点个数
//当前点的值
int ch[];
//左右儿子,0为左儿子,1为右儿子
} t[N];
关于获得这个节点是左儿子还是右儿子:
inline int get(int x) {
return t[t[x].fa].ch[] == x ? : ;
}
更新:
inline void pushup(int x) {
t[x].sum = t[t[x].ch[]].sum + t[t[x].ch[]].sum + t[x].cnt;
}
旋转:
inline void rotate(int x) {
int fa = t[x].fa, gfa = t[fa].fa;//父亲和爷爷
int k = get(x);//x是其父节点的那个儿子
//step1
t[x].fa = gfa;
t[gfa].ch[get(fa)] = x;
//step2
t[t[x].ch[k ^ ]].fa = fa;
t[fa].ch[k] = t[x].ch[k ^ ];
//step3
t[fa].fa = x;
t[x].ch[k ^ ] = fa;
pushup(fa), pushup(x);
//因为旋转后父节点成了当前点的子节点,所以先更新父亲
}
关于为什么是 k ^ 1,假设我们要旋转的点是左儿子,那他的父亲一定会成为他的右儿子,同理,如果要旋转的点是左儿子,他的父节点一定会成为他的右儿子
伸展(splay)
splay操作就是把一个点旋转到指定的点
最容易想到的,就是一直旋转到指定的节点,然而这样是错的
这时我们就要用到双旋,双旋有两大种四小种情况
1、zig-zig或zag-zag
当节点是父亲的左儿子且父节点是祖父节点的左儿子
或节点是父亲的右儿子且父节点是祖父节点的右儿子
先旋转父亲,再旋转自己
借用一下GeeksofrGeeks的图:
Zig-Zig (Left Left Case):
G P X
/ \ / \ / \
P T4 rightRotate(G) X G rightRotate(P) T1 P
/ \ ============> / \ / \ ============> / \
X T3 T1 T2 T3 T4 T2 G
/ \ / \
T1 T2 T3 T4 Zag-Zag (Right Right Case):
G P X
/ \ / \ / \
T1 P leftRotate(G) G X leftRotate(P) P T4
/ \ ============> / \ / \ ============> / \
T2 X T1 T2 T3 T4 G T3
/ \ / \
T3 T4 T1 T2
2.zig-zag或zag-zig
当节点是父亲的左儿子且父节点是祖父节点的右儿子
或节点是父亲的右儿子且父节点是祖父节点的左儿子
旋转两次自己
再次借用GeeksforGeeks的图:
Zag-Zig (Left Right Case):
G G X
/ \ / \ / \
P T4 leftRotate(P) X T4 rightRotate(G) P G
/ \ ============> / \ ============> / \ / \
T1 X P T3 T1 T2 T3 T4
/ \ / \
T2 T3 T1 T2 Zig-Zag (Right Left Case):
G G X
/ \ / \ / \
T1 P rightRotate(P) T1 X leftRotate(P) G P
/ \ =============> / \ ============> / \ / \
X T4 T2 P T1 T2 T3 T4
/ \ / \
T2 T3 T3 T4
代码:
inline void splay(int x, int pos) {
while (t[x].fa != pos) {//一直旋转成为目标位置的儿子
int fa = t[x].fa, gfa = t[fa].fa;
if (gfa != pos) (t[gfa].ch[] == fa) ^ (t[fa].ch[] == x) ? rotate(x) : rotate(fa);//判断是哪个儿子并旋转
rotate(x);//无论哪种情况都要旋转x
}
if (pos == ) root = x;
}
插入(insert)
对于一个新的值x
如果x等于根的值,从根节点开始比较节点的val值
如果x==val的话,这个点的计数器++,
x小于val的话向左搜,x大于val的话向右搜
如果不存在某个点的val是x,这时我们即使搜到最底端也没有找到,就直接新建这个节点
因为在插入时可能会形成一条链,在最后的时候还要splay一下把新插入的节点转为根
代码:
inline void insert(int x) {
int u = root, fa = ; //当前位置u,父节点fa
while (u && t[u].val != x) { //当u不存在且u的值不等于x。······①
fa = u; //向下找u的儿子,父亲为u
u = t[u].ch[x > t[u].val]; //大于当前位置u向右找,小于向左找
}
if (u) t[u].cnt++; //如果有一个节点的值等于x,计数器++
else {
u = ++tot; //新节点的位置
if (fa) t[fa].ch[x > t[fa].val] = u; //如果父节点非根
t[u].ch[] = t[u].ch[] = ; //没有儿子
t[u].fa = fa, t[u].val = x, t[u].cnt = , t[u].sum = ;
}
splay(u, ); //旋转保持树的平衡
}
查找(find)
与操作插入操作相似
只需要向左右子树找所查找的数
如果当前点的值等于所查找的数,把当前节点splay到根
inline void find(int x) { //查找x的位置并旋转到根
int u = root;
if (!u) return ; //空树
while (t[u].ch[x > t[u].val] && x != t[u].val) //存在儿子且当前节点的值不等于x。······②
u = t[u].ch[x > t[u].val];//跳转到儿子
splay(u, ); //旋转到根
}
在初学的时候在这里糊了一下,在这里稍微说明
在insert和find中,一个是当u存在(①),一个是当u的儿子存在(②),当时还试着改了一下代码,结果
其实也很简单
在插入的时候,如果没有一个节点的值等于x,我们在找的时候u会找到树外(u为0,就表示了这个节点不存在),这时我们就新建节点
在查找的时候,不能找出树外,所以要判断u对应的子节点是否存在,不能让u跑到树外面
前驱/后继(nx)
先find一下,把要找的数先转到根
以后继为例,确定后继比x大,所以在右子树里找
有因为后继是右子树里最小的,就在右子树一直向左找,找到叶节点
前驱相反
inline int nx(int x, int f) { //0 next;1 pre
find(x);
int u = root;
if (t[u].val > x && f) return u;//如果当前节点的值大于x并且要查找的是后继
if (t[u].val < x && !f) return u;//如果当前节点的值小于x并且要查找的是前驱
u = t[u].ch[f]; //前驱在左子树里找,后继在右子树里找
while (t[u].ch[f ^ ]) u = t[u].ch[f ^ ];//在另一个方向上找
return u;
}
第k小的数(rank)
先判断一下是不是有这么多数
看一下左子树的大小,如果k小于左子树大小的话就在左子树里找第k小
如果k大于(左子树大小+当前节点的个数),在右子树上找第(k-左子树大小-当前节点的个数)小
否则,就是根节点的值
inline int rank(int x) {
int u = root;
if (t[u].sum < x) return ; //没有这么多节点
while () {
int v = t[u].ch[]; //左子树
if (x > t[v].sum + t[u].cnt) { //如果排名大于左子树的大小+当前节点的数量
x -= t[v].sum + t[u].cnt;
u = t[u].ch[]; //当前排名的数一定在右儿子上
} else if (t[v].sum >= x) u = v; //在左子树上
else return t[u].val; //根节点
}
}
删除(Del)
删除一个点的话
把前驱转到根,把后继转到前驱的下面
后继比前驱大,在前驱的右子树,当前数比前驱大,在前驱的右子树
而在右子树内比后继小的只有当前数,在后继的左子树,所以直接删去后继的左子树
inline void Del(int x) {
int last = nx(x, ), nxt = nx(x, ); //前驱,后继
splay(last, ), splay(nxt, last);
int del = t[nxt].ch[]; //后继的左子树
if (t[del].cnt > ) { //超过一个
t[del].cnt--; //计数--
splay(del, );
} else t[nxt].ch[] = ; //删除
}
模板:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + ;
int n, m, tot, root;
struct tree {
int fa, cnt, sum, val;
int ch[];
} t[N]; template<class T>inline void read(T &x) {
x = ; int f = ; char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) x = x * + ch - '', ch = getchar();
x = f ? -x : x;
return ;
} inline int get(int x) {
return t[t[x].fa].ch[] == x ? : ;
} inline void pushup(int x) {
t[x].sum = t[t[x].ch[]].sum + t[t[x].ch[]].sum + t[x].cnt;
} inline void rotate(int x) {
int fa = t[x].fa, gfa = t[fa].fa;
int k = get(x); t[x].fa = gfa;
t[gfa].ch[get(fa)] = x; t[t[x].ch[k ^ ]].fa = fa;
t[fa].ch[k] = t[x].ch[k ^ ]; t[fa].fa = x;
t[x].ch[k ^ ] = fa;
pushup(fa), pushup(x);
} inline void splay(int x, int pos) {
while (t[x].fa != pos) {
int fa = t[x].fa, gfa = t[fa].fa;
if (gfa != pos) (t[fa].ch[] == x) ^ (t[gfa].ch[] == fa) ? rotate(x) : rotate(fa);
rotate(x);
}
if (pos == ) root = x;
} inline void find(int x) { //查找x的位置并旋转到根
int u = root;
if (!u) return ;
while (t[u].ch[x > t[u].val] && x != t[u].val) u = t[u].ch[x > t[u].val];
splay(u, );
} inline void insert(int x) {
int u = root, fa = ; //当前位置u,u的父节点ff
while (u && t[u].val != x) {
fa = u;
u = t[u].ch[x > t[u].val];
}
if (u) t[u].cnt++;
else {
u = ++tot;
if (fa) t[fa].ch[x > t[fa].val] = u; //如果父节点非根
t[u].ch[] = t[u].ch[] = ; //没有儿子
t[u].fa = fa, t[u].val = x, t[u].cnt = , t[u].sum = ;
}
splay(u, );
} inline int nx(int x, int f) { //0 next;1 pre
find(x);
int u = root;
if (t[u].val > x && f) return u;
if (t[u].val < x && !f) return u;
u = t[u].ch[f]; //后继往左找,前驱往右找
while (t[u].ch[f ^ ]) u = t[u].ch[f ^ ];
return u;
} inline int rank(int x) {
int u = root;
if (t[u].sum < x) return ;
while () {
int v = t[u].ch[];
if (x > t[v].sum + t[u].cnt) { //如果排名比左儿子的大小和当前节点的数量要大
x -= t[v].sum + t[u].cnt; //那么当前排名的数一定在右儿子上找
u = t[u].ch[];
} else if (t[v].sum >= x) u = v;
else return t[u].val;
}
} inline void Del(int x) {
int last = nx(x, ), nxt = nx(x, );
splay(last, ), splay(nxt, last);
int del = t[nxt].ch[];
if (t[del].cnt > ) {
t[del].cnt--;
splay(del, );
} else t[nxt].ch[] = ;
} int main(int argc, char const *argv[]) {
insert(), insert(-);
read(n);
while (n --) {
int opt, k;
read(opt);
if (opt == ) read(k), insert(k);
else if (opt == ) read(k), Del(k);
else if (opt == ) {
read(k);
find(k);
printf("%d\n", t[t[root].ch[]].sum);
}
else if (opt == ) {
read(k);
printf("%d\n", rank(k + ));
}
else if (opt == ) {
read(k);
printf("%d\n", t[nx(k, )].val);
}
else if (opt == ) {
read(k);
printf("%d\n", t[nx(k, )].val);
}
}
return ;
}
Splay模板
普通Splay详解的更多相关文章
- 在洛谷3369 Treap模板题 中发现的Splay详解
本题的Splay写法(无指针Splay超详细) 前言 首先来讲...终于调出来了55555...调了整整3天..... 看到大部分大佬都是用指针来实现的Splay.小的只是按照Splay的核心思想和原 ...
- splay详解(一)
前言 Spaly是基于二叉查找树实现的, 什么是二叉查找树呢?就是一棵树呗:joy: ,但是这棵树满足性质—一个节点的左孩子一定比它小,右孩子一定比它大 比如说 这就是一棵最基本二叉查找树 对于每次插 ...
- splay详解(二)
前言 在上一节中,我们讲述了Splay的核心操作rotate与splay 本节我会教大家如何用这两个函数实现各种强大的功能 为了方便讲解,我们拿这道题做例题来慢慢分析 利用splay实现各种功能 首先 ...
- splay详解(三)
前言 上一节我们学习了splay所能解决的基本问题,这节我来讲一下splay怎么搞区间问题 实现 splay搞区间问题非常简单,比如我们要在区间$l,r$上搞事情,那么我们首先把$l$的前驱旋转到根节 ...
- Splay详解
平衡树实际很简单的 以下讲解都以Luogu P3369 [模板]普通平衡树为例 我不会带指针的Splay,所以我就写非指针型的Splay Splay是基于二叉查找树(bst)实现的 什么是二叉查找树呢 ...
- [转载]Splay Tree数组实现+详解
变量声明:f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的关键字(即结点i代表的那个数字),cnt[i]表示i结点的关键字出现的次数(相当于 ...
- Link-Cut-Tree详解
图片参考YangZhe的论文,FlashHu大佬的博客 Link-Cut-Tree实际靠的是实链剖分,重链剖分和长链剖分珂以参考树链剖分详解 Link-Cut-Tree将某一个儿子的连边划分为实边,而 ...
- Linq之旅:Linq入门详解(Linq to Objects)
示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...
- 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)
一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...
随机推荐
- Python-正则复习-56
# 正则表达式# 字符组 [字符]# 元字符 # \w \d \s # \W \D \S # . 除了换行符以外的任意字符 # \n \t # \b # ^ $ 匹配字符串的开始和结束 # () 分组 ...
- new、getInstance()、newInstance()、Class.forName()
1.对象使用之前通过getinstance()得到而不需要自己定义,用完之后不需要delete: 2.new 一定要生成一个新对象,分配内存:getInstance() 则不一定要再次创建,它可以把一 ...
- Graph Without Long Directed Paths CodeForces - 1144F (dfs染色)
You are given a connected undirected graph consisting of nn vertices and mm edges. There are no self ...
- Open Live Writer安装教程
配置步骤: 1.在菜单中选择"工具">"帐户",出现下面的画面: 2.点击"添加按钮",在出现的窗口中选择"其他日志服务&q ...
- iOS-带图片的二维码的生成(QRCode)
https://blog.csdn.net/feng512275/article/details/82824650 2018年09月23日 20:29:45 筝风放风筝 阅读数:91 版权声明:本 ...
- 分布式ID生成系统 UUID与雪花(snowflake)算法
Leaf——美团点评分布式ID生成系统 -https://tech.meituan.com/MT_Leaf.html 网游服务器中的GUID(唯一标识码)实现-基于snowflake算法-云栖社区-阿 ...
- MySQL数据类型优化—整数类型优化选择
原文:http://bbs.landingbj.com/t-0-240002-1.html 在设计数据库的时候,整数类型的使用时不可避免的如ID,类型等. 在选择整数的同时主要是考虑是数据范围,如是否 ...
- React Native之获取通讯录信息并实现类通讯录列表(ios android)
React Native之获取通讯录信息并实现类通讯录列表(ios android) 一,需求分析 1,获取通讯录信息,筛选出通讯录里有多少好友在使用某个应用. 2,获取通讯录信息,实现类通讯录,可拨 ...
- css太极
自己用css做的太极,留个纪念. 用css做太极有很多种实现方法,我这种大概是最简单的了吧,因为div用得太多了,哈哈. 高级一点的应该是用伪类:before和:after去减少div的用量(手动滑稽 ...
- vue cli3 vue.config.js 配置详情
module.exports = { // 基本路径 baseUrl: process.env.NODE_ENV === 'production' ? '/' : '/', ...