Splay学习笔记,每个操作都会执行splay。
文章目录
- 前言
- 平衡树
- Zig和Zag
- 引入splay操作
- Splay的核心函数
- 查找前驱和后继
- 查找排名和第k小
- 插入和删除
- 完结感言
前言
之前学了fhq—Treap,一种靠分裂与合并维护平衡的一种树,期望复杂度是logn,常数也较大。Treap也有带旋转版本的,但是为了早日学会LCT还是先学Splay。也建议初学者先学不带旋转的平衡树,当然还有更简单的替罪羊树也可以先学。先借简单的来了解为什么要这样做。然后学更难的就跟好理解了。
平衡树
BST(二叉搜索树)的缺点是容易被卡成链,所以我们想要通过某种方法来使它的树高不那么任意被卡。我理解的平衡树就是为了使BST保持平衡,即树的高度期望是logn,或者每种操作均摊logn。保证了复杂度的情况下,就可以把平衡树当作二叉搜索树来用拉。Treap借助的是Heap,使他的树高的期望是logn。而Splay利用的是旋转,让它的树形始终在变化(以万变应不变),使得每个操作均摊logn(证明是用了势能分析,很多博客都讲了证明方法,这里不说了。其实是不会 )。只是自己的拙见,若有不对还望大佬指出。
Zig和Zag
我们把splay最基本的操作称为Zig(左旋)和Zag(右旋),每次旋转后保证它的中序遍历不变。(treap每次操作也是保证中序遍历不变)。对于BST来说有用的只有中序遍历,因为中序遍历可以得出有序的序列。所以我们也必须要保证中序遍历不变。理解左旋和右旋一定要从中序遍历不变的角度理解。
中序遍历是axbycz,经过转换后中序遍历不变。
可以把(a,x),(b),(y,c)当成三个不同的部分,走完x之后下一个要走的是b,而(y,c)已经成为了右儿子,为了先去b,把b连到y的左儿子处。
左旋也是类似,这里就放个图不解释了。
Zig和Zag做的事情就是把当前的节点往上旋,把父节点变成你的子节点(认子做父?)。
我们先给出代码:
struct node {int ch[2], fa;// ch[0]为左儿子, ch[1]为右儿子,fa为父亲
}T[N];
void rotate(int x) {int y = T[x].fa; // 父亲节点 int z = T[y].fa; // 祖父节点 int k = T[x].ch[1] == x; //判断是左儿子还是右儿子T[z].ch[T[z].ch[1]==y] = x; T[x].fa = z;// 认祖父当爹 T[y].ch[k] = T[x].ch[k^1]; T[T[x].ch[k^1]]=y; // 认孙子当儿子 T[x].ch[k^1] = y; T[y].fa = x; // 认子当爹
}
建议大家理解了左旋右旋后自己独立写几遍。
引入splay操作
splay操作才是Splay最核心的部分。我们观察上面的Zig和Zag,我们把x旋到根,如果我们只对x这个点执行两次Zig或者只执行两次Zag,你会发现它的最长链始终没有改变。这样的话Splay就容易被卡。所以一般都是分不同的情况选择旋转的点,例如第一个图,我们先旋转y,再旋转x。你会发现最长链没了(变成了另外一条,虽然长度看起来一样长)。
总的来说,总共就两种情况,一种是三个点都在同一侧。如下图
我们先旋转y,再旋转x。
另外一种就是不在同一侧。如下图
我们旋转两次x即可。还有一些情况是没有z节点的,我们直接旋转x就行。
然后我们发现无论怎么样都会执行一次旋转x,只需要判断是否需要先旋转y即可。
给出代码:
int root; // 全局根节点
void splay(int x, int rt) { // 把x旋转到rt的儿子,若rt为0则旋转到根while(T[x].fa != rt) { // 若没有旋到rt的儿子 int y = T[x].fa, z = T[y].fa; // 找父节点和祖父节点 if(z != rt) (T[z].ch[0] == y) ^ (T[y].ch[0] == x) ? rotate(x), rotate(y); // 若有三个点则按照上面分析的旋转 rotate(x);// 无论怎么样都要旋转x } if(rt == 0) root = x;
}
然后剩下的就是一些Splay要注意的地方了。每次操作都执行splay函数,除非是用不上的,例如第k大查询。这样做是为了均摊复杂度。感性理解就是虽然某一刻可能变成了链,但是下一刻就改变了,除非写挂了,不然数据是卡不掉的。
我们就用P3369 【模板】普通平衡树 来讲一下各个操作的要点。
Splay的核心函数
上传标记和下传标记和线段树的方法是一样的在变化的地方上传,被改过且要用的地方下传。这里不涉及下传,就不写了。
struct node {int ch[2], fa;// ch[0]为左儿子, ch[1]为右儿子,fa为父亲int val, cnt, siz; // 该点的权值,该点重复的次数,它及它儿子的数的数量
}T[N];
int root, cnt; // 全局根节点
void up(int rt) {T[rt].siz = T[rt].cnt + T[T[rt].ch[0]].siz + T[T[rt].ch[1]].siz;
}
void rotate(int x) {int y = T[x].fa; // 父亲节点 int z = T[y].fa; // 祖父节点 int k = T[y].ch[1] == x; //判断是左儿子还是右儿子T[z].ch[T[z].ch[1]==y] = x; T[x].fa = z;// 认祖父当爹 T[y].ch[k] = T[x].ch[k^1]; T[T[x].ch[k^1]].fa=y; // 认孙子当儿子 T[x].ch[k^1] = y; T[y].fa = x; // 认子当爹 up(y); up(x); //只有x和y需要更新
}
void splay(int x, int rt) { // 把x旋转到rt的儿子,若rt为0则旋转到根while(T[x].fa != rt) { // 若没有旋到rt的儿子 int y = T[x].fa, z = T[y].fa; // 找父节点和祖父节点 if(z != rt) (T[z].ch[0] == y) ^ (T[y].ch[0] == x) ? rotate(x): rotate(y); // 若有三个点则按照上面分析的旋转 rotate(x);// 无论怎么样都要旋转x } if(rt == 0) root = x;
}
int new_node(int x, int fa) {int rt = ++cnt;if(fa) T[fa].ch[x > T[fa].val] = rt;T[rt].ch[0] = T[rt].ch[1] = 0;T[rt].fa = fa;T[rt].siz = T[rt].cnt = 1;T[rt].val = x;return rt;
}
注意:为了方便起见,我们默认插入了一个负无穷和正无穷
查找前驱和后继
我们先从简单的操作开始,首先是查找前驱和后继。
前驱和后继的方法差不多,由于可能没有前驱和后继,插入正负无穷就可以少判断很多情况。你会发现查找前驱和后继都记录了fa,这个变量在删除的时侯有用。最后记得splay
int pre(int x) { // 查找前驱 int fa, an;for(int rt(root); rt; ) {if(T[rt].val >= x) rt = T[rt].ch[0];else an = T[fa = rt].val, rt = T[rt].ch[1];}splay(fa, 0);return an;
}
int nxt(int x) { // 查找后继 int fa=root, an;for(int rt(root); rt; ) { if(T[rt].val <= x) rt = T[rt].ch[1];else an = T[fa=rt].val, rt = T[rt].ch[0];} splay(fa, 0);return an;
}
查找排名和第k小
然后是查找排名和查找k小数。
查找x的排名的时侯,由于可能没有这个数,所以我用一个变量fa记录了它的父节点,这样最后就可以splay一下了。
int _rank(int x) { // 找到数x是第几小 int k(0), rt(root), fa = rt;while(rt) { fa = rt;if(T[rt].val == x) { splay(rt, 0); return T[T[rt].ch[0]].siz; } else if(T[rt].val < x) k += T[rt].cnt + T[T[rt].ch[0]].siz, rt = T[rt].ch[1]; else rt = T[rt].ch[0]; } if(!fa) splay(fa, 0);// 虽然一定找得到,但还是保险起见return k;
}
int _kth(int x) { // 第x小的数是多少,这个要是超出了总数,直接return吧,不然就一定会splayif(x > T[root].siz) return INF;int k(1);for(int rt(root); rt;) {if(k + T[T[rt].ch[0]].siz <= x and x < k + T[rt].cnt + T[T[rt].ch[0]].siz) {splay(rt, 0); return T[rt].val;} else if(x < k + T[T[rt].ch[0]].siz) rt = T[rt].ch[0];else k += T[T[rt].ch[0]].siz + T[rt].cnt, rt = T[rt].ch[1];}
}
插入和删除
然后是插入和删除。splay最恶心的就是删除操作,要考虑很多情况,比如这个数不存在,如果不存在不是简简单单的return,你要判断它是否存在肯定要去遍历树,而遍历了树你就得splay一下,由于这个点不存在,所以你要记录它的父亲,想想就挺麻烦的。然后在网上找到了一个很精简的写法。找到前驱和后继,把后继splay到前驱的儿子处,然后你会发现该点一定在后继节点的左儿子,如果它存在的话。不存在就直接splay一下后继就行。存在的话看一下它是不是大于1个,若是则直接减一就行,否则删除这个节点。虽然复杂度高了点,但是好写多了。 这里也能看到先插入正负无穷的好处,就是前驱和后继一定存在。
比较简短的代码:
void insert(int x) { int rt = root, fa = 0;while(rt && T[rt].val != x) { fa = rt;rt = T[rt].ch[x > T[rt].val];} if(rt) T[rt].cnt++;else rt = new_node(x, fa);splay(rt, 0);
}
void del(int x) { nxt(x); int ne = root;pre(x); int la = root;splay(ne, la);int del = T[ne].ch[0];if(T[del].val != x) return ;if(T[del].cnt > 1) splay(del, 0), T[del].cnt--, T[del].siz--;else T[ne].ch[0] = 0, splay(ne, 0);
}
完整代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const double Pi = acos(-1);
namespace {template <typename T> inline void read(T &x) {x = 0; T f = 1;char s = getchar();for(; !isdigit(s); s = getchar()) if(s == '-') f = -1;for(; isdigit(s); s = getchar()) x = (x << 3) + (x << 1) + (s ^ 48);x *= f;}
}
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define _for(n,m,i) for (register int i = (n); i < (m); ++i)
#define _rep(n,m,i) for (register int i = (n); i <= (m); ++i)
#define _srep(n,m,i)for (register int i = (n); i >= (m); i--)
#define _sfor(n,m,i)for (register int i = (n); i > (m); i--)
#define lson rt << 1, l, mid
#define rson rt << 1 | 1, mid + 1, r
#define lowbit(x) x & (-x)
#define pii pair<int,int>
#define fi first
#define se second
const int N = 1e5+5;
struct node {int ch[2], fa;// ch[0]为左儿子, ch[1]为右儿子,fa为父亲int val, cnt, siz; // 该点的权值,该点重复的次数,它及它儿子的数的数量
}T[N];
int root, cnt; // 全局根节点
void up(int rt) {T[rt].siz = T[rt].cnt + T[T[rt].ch[0]].siz + T[T[rt].ch[1]].siz;
}
void rotate(int x) {int y = T[x].fa; // 父亲节点 int z = T[y].fa; // 祖父节点 int k = T[y].ch[1] == x; //判断是左儿子还是右儿子T[z].ch[T[z].ch[1]==y] = x; T[x].fa = z;// 认祖父当爹 T[y].ch[k] = T[x].ch[k^1]; T[T[x].ch[k^1]].fa=y; // 认孙子当儿子 T[x].ch[k^1] = y; T[y].fa = x; // 认子当爹 up(y); up(x); //只有x和y需要更新
}
void splay(int x, int rt) { // 把x旋转到rt的儿子,若rt为0则旋转到根while(T[x].fa != rt) { // 若没有旋到rt的儿子 int y = T[x].fa, z = T[y].fa; // 找父节点和祖父节点 if(z != rt) (T[z].ch[0] == y) ^ (T[y].ch[0] == x) ? rotate(x): rotate(y); // 若有三个点则按照上面分析的旋转 rotate(x);// 无论怎么样都要旋转x } if(rt == 0) root = x;
}
int new_node(int x, int fa) {int rt = ++cnt;if(fa) T[fa].ch[x > T[fa].val] = rt;T[rt].ch[0] = T[rt].ch[1] = 0;T[rt].fa = fa;T[rt].siz = T[rt].cnt = 1;T[rt].val = x;return rt;
}
void insert(int x) { int rt = root, fa = 0;while(rt && T[rt].val != x) { fa = rt;rt = T[rt].ch[x > T[rt].val];} if(rt) T[rt].cnt++;else rt = new_node(x, fa);splay(rt, 0);
}
int _rank(int x) { // 找到数x是第几小 int k(0), rt(root), fa = rt;while(rt) { fa = rt;if(T[rt].val == x) { splay(rt, 0); return T[T[rt].ch[0]].siz; } else if(T[rt].val < x) k += T[rt].cnt + T[T[rt].ch[0]].siz, rt = T[rt].ch[1]; else rt = T[rt].ch[0]; } if(!fa) splay(fa, 0);return k;
}
int _kth(int x) { // 第x小的数是多少 if(x > T[root].siz) return INF;int k(1);for(int rt(root); rt;) { if(k + T[T[rt].ch[0]].siz <= x and x < k + T[rt].cnt + T[T[rt].ch[0]].siz) {splay(rt, 0); return T[rt].val;} else if(x < k + T[T[rt].ch[0]].siz) rt = T[rt].ch[0];else k += T[T[rt].ch[0]].siz + T[rt].cnt, rt = T[rt].ch[1];}
}
int pre(int x) { // 查找前驱 int fa, an;for(int rt(root); rt; ) {if(T[rt].val >= x) rt = T[rt].ch[0];else an = T[fa = rt].val, rt = T[rt].ch[1];}splay(fa, 0);return an;
}
int nxt(int x) { // 查找后继 int fa=root, an;for(int rt(root); rt; ) { if(T[rt].val <= x) rt = T[rt].ch[1];else an = T[fa=rt].val, rt = T[rt].ch[0];} splay(fa, 0);return an;
}
void del(int x) { nxt(x); int ne = root;pre(x); int la = root;splay(ne, la);int del = T[ne].ch[0];if(T[del].val != x) return ;if(T[del].cnt > 1) splay(del, 0), T[del].cnt--, T[del].siz--;else T[ne].ch[0] = 0, splay(ne, 0);
}
int main() { insert(-INF);insert(INF);int n, op, x; read(n);while(n--) { read(op); read(x);switch(op) { case 1: insert(x); break;case 2: del(x); break;case 3: printf("%d\n", _rank(x)); break;case 4: printf("%d\n", _kth(x+1)); break;case 5: printf("%d\n", pre(x)); break;case 6: printf("%d\n", nxt(x)); break;} }
}
完结感言
感觉Splay比fhq-Treap难写多了,要注意的地方也很多。但是Splay很灵活,所以它被选为LCT的辅助树,学这个也是为了学LCT。马上滚去学LCT了
Splay学习笔记,每个操作都会执行splay。相关推荐
- memcached高速缓存学习笔记002---telnet操作memcached
memcached高速缓存学习笔记002---telnet操作memcached 停止memcached memcached.exe -d stop 停止 memcached.exe -p 112 ...
- Apifox 学习笔记 - 前置操作
Apifox 学习笔记 - 前置操作 设置 Content-Length 参考资料 设置 Content-Length Content-Type: application/x-www-form-url ...
- js学习笔记82——操作内联样式
js学习笔记82--操作内联样式 通过js修改元素的样式 查参考手册 内联样式 读取元素的样式 看如下代码 <!DOCTYPE html> <html lang="en&q ...
- 学习笔记:操作系统启动过程
学习笔记:操作系统启动过程 参考资料: 1.<操作系统真象还原>郑钢 2.<操作系统引导探究> 谢煜波 操作系统启动过程 按下电源后: 电源键连接的电信号线发送一个电信号给主板 ...
- 文艺平衡树 Splay 学习笔记(1)
(这里是Splay基础操作,reserve什么的会在下一篇里面讲) 好久之前就说要学Splay了,结果苟到现在才学习. 可能是最近良心发现自己实在太弱了,听数学又听不懂只好多学点不要脑子的数据结构. ...
- 可持久化Splay 学习笔记
可持久化Splay是怎么回事呢?Splay相信大家都很熟悉,但是可持久化Splay是怎么回事呢,下面就让小编带大家一起了解吧. 可持久化Splay,其实就是将Splay持久化一下,大家可能会很惊讶Sp ...
- PHP学习笔记-文件操作1
转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/52294237 本文出自:[顾林海的博客] 前言 PHP支持文件上传功能 ...
- PHP学习笔记-字符串操作1
转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/51001820 本文出自:[顾林海的博客] 前言 这几天身体比较疲惫,看 ...
- 【安全牛学习笔记】操作系统识别
该笔记为安全牛课堂学员笔记,想看此课程或者信息安全类干货可以移步到安全牛课堂 Security+认证为什么是互联网+时代最火爆的认证? 牛妹先给大家介绍一下Security+ Security+ 认证 ...
最新文章
- 我用分布式事务干掉了一摞简历
- R语言使用coin包应用于独立性问题的置换检验(permutation tests)、使用普通cor.test函数和置换近似spearman_test函数、检验变量的相关性的显著性
- 算法 求两个自然数的最小公倍数 C
- 【JAVA SE】第八章 异常处理与抽象类
- 2.Linux环境下配置Solr4.10.3
- error C2712: Cannot use __try in functions that require object unwinding
- java 数组扩容_Java数组扩容算法及Java对它的应用
- 应用 Valgrind 发现 Linux 程序的内存问题
- 软件项目设计文档分类
- ion-infinite-scroll上拉加载 ion-refresher下拉刷新
- Luminati动态住宅IP使用教程_AdsPower防关联浏览器软件教程(二)
- CSS中文字间距和行间距
- 互联网公司技术总监工作内容
- linux查看磁带机端口,linux、unix下使用磁带机的常用命令
- 蓝桥杯JAVA答题技巧,第九届蓝桥杯大赛个人赛省赛(软件类)C/C++ 大学B组比赛心得(还在更新)...
- 微信小程序 - eCharts- x轴换行和旋转45°
- BUUCTF 认真你就输了
- K8S Calico网络插件之IPIP模式
- scrapy快速入门
- ArcGIS API for JavaScript 4.0尝鲜——WebGIS前端开发大杀器