文章目录

  • 前言
  • 平衡树
  • 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。相关推荐

  1. memcached高速缓存学习笔记002---telnet操作memcached

    memcached高速缓存学习笔记002---telnet操作memcached 停止memcached  memcached.exe  -d stop 停止 memcached.exe -p 112 ...

  2. Apifox 学习笔记 - 前置操作

    Apifox 学习笔记 - 前置操作 设置 Content-Length 参考资料 设置 Content-Length Content-Type: application/x-www-form-url ...

  3. js学习笔记82——操作内联样式

    js学习笔记82--操作内联样式 通过js修改元素的样式 查参考手册 内联样式 读取元素的样式 看如下代码 <!DOCTYPE html> <html lang="en&q ...

  4. 学习笔记:操作系统启动过程

    学习笔记:操作系统启动过程 参考资料: 1.<操作系统真象还原>郑钢 2.<操作系统引导探究> 谢煜波 操作系统启动过程 按下电源后: 电源键连接的电信号线发送一个电信号给主板 ...

  5. 文艺平衡树 Splay 学习笔记(1)

    (这里是Splay基础操作,reserve什么的会在下一篇里面讲) 好久之前就说要学Splay了,结果苟到现在才学习. 可能是最近良心发现自己实在太弱了,听数学又听不懂只好多学点不要脑子的数据结构. ...

  6. 可持久化Splay 学习笔记

    可持久化Splay是怎么回事呢?Splay相信大家都很熟悉,但是可持久化Splay是怎么回事呢,下面就让小编带大家一起了解吧. 可持久化Splay,其实就是将Splay持久化一下,大家可能会很惊讶Sp ...

  7. PHP学习笔记-文件操作1

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/52294237 本文出自:[顾林海的博客] 前言 PHP支持文件上传功能 ...

  8. PHP学习笔记-字符串操作1

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/51001820 本文出自:[顾林海的博客] 前言 这几天身体比较疲惫,看 ...

  9. ​【安全牛学习笔记】操作系统识别

    该笔记为安全牛课堂学员笔记,想看此课程或者信息安全类干货可以移步到安全牛课堂 Security+认证为什么是互联网+时代最火爆的认证? 牛妹先给大家介绍一下Security+ Security+ 认证 ...

最新文章

  1. 我用分布式事务干掉了一摞简历
  2. R语言使用coin包应用于独立性问题的置换检验(permutation tests)、使用普通cor.test函数和置换近似spearman_test函数、检验变量的相关性的显著性
  3. 算法 求两个自然数的最小公倍数 C
  4. 【JAVA SE】第八章 异常处理与抽象类
  5. 2.Linux环境下配置Solr4.10.3
  6. error C2712: Cannot use __try in functions that require object unwinding
  7. java 数组扩容_Java数组扩容算法及Java对它的应用
  8. 应用 Valgrind 发现 Linux 程序的内存问题
  9. 软件项目设计文档分类
  10. ion-infinite-scroll上拉加载 ion-refresher下拉刷新
  11. Luminati动态住宅IP使用教程_AdsPower防关联浏览器软件教程(二)
  12. CSS中文字间距和行间距
  13. 互联网公司技术总监工作内容
  14. linux查看磁带机端口,linux、unix下使用磁带机的常用命令
  15. 蓝桥杯JAVA答题技巧,第九届蓝桥杯大赛个人赛省赛(软件类)C/C++ 大学B组比赛心得(还在更新)...
  16. 微信小程序 - eCharts- x轴换行和旋转45°
  17. BUUCTF 认真你就输了
  18. K8S Calico网络插件之IPIP模式
  19. scrapy快速入门
  20. ArcGIS API for JavaScript 4.0尝鲜——WebGIS前端开发大杀器

热门文章

  1. android彩蛋长按无反应,Android TextView长按复制功能失效解决办法
  2. WPF 图片头像自由剪切器实时截图细节放大器
  3. PHP 淘宝API发布产品 taobao.item.add
  4. ZEMAX光学设计视频教程 ZEMAX资料教程大全
  5. CentOS服务端命令大全
  6. 路侧激光雷达目标检测系统-篇1
  7. 兰手指模拟器( BlueStacks)又不听话了,不能运行的解决办法
  8. 2023全国大学生英语竞赛C类试卷
  9. SLI导致双显卡被TensorFlow同时占用问题(Windows下)
  10. FPS枪法练习!献给所有热爱FPS游戏的玩家们