【笔记】Splay

【笔记】Splay 目录

      • 简介
        • 右旋
        • 左旋
      • 核心思想
      • 操作
        • a. Splay
        • b. 插入
        • c. 删除
      • 信息的维护
      • 例题
        • AcWing 2437. Splay
        • P3369 【模板】普通平衡树


简介

Splay 是一种平衡树,并且是一棵二叉搜索树(BST)。

它满足对于任意节点,都有左子树上任意点的值 < 当前节点的值 < 右子树上任意点的值。

优点:支持多种操作。
缺点:常数较大。

单次操作均摊复杂度 O ( log ⁡ n ) O(\log n) O(logn)

(注:关于 Splay 单次操作均摊复杂度的证明见 OI-Wiki)

Splay 基于旋转操作维护树的平衡。旋转分为左旋 (zag) 和右旋 (zig)。

旋转,即在保证平衡树中序遍历不变的前提下,改变整个树的深度。

右旋

zig

对于单次操作 zig(a) ,将节点 a a a 左儿子的左儿子( c c c)接到 a a a 的左儿子处,将 c c c 的右儿子接到 b b b 的左儿子,将 c c c 的右儿子改为 b b b

这样,我们完成了一次右旋操作,操作前后, a a a 左子树的中序遍历都为 DcEbF

左旋

zag

左旋即右旋的逆过程,将每一步反过来即可。


核心思想

每次操作之后,都将被操作的节点旋转至根节点。

这个和均摊时间复杂度有关。

操作

a. Splay

每次调用函数 splay(x, k) 表示把点 x x x 旋转至点 k k k 下方。

特别地,当调用 splay(x, 0) 时,表示把点 x x x 旋转至根节点。

有两种情况:

  1. x , y , z x, y,z x,y,z 成一条链

1

  1. x , y , z x,y,z x,y,z 不成一条链

2

b. 插入
  • 根据 BST 性质,找到该元素所在位置并新建节点。插入后将该节点旋转至根节点。
  • 当要求将一个序列插到 y y y 的后面时:
    1. 找到 y y y 的后继 z z z
    2. y y y 转到根。(splay(y, 0);
    3. z z z 转到 y y y 的下方。(splay(z, y);)由于 z > y z>y z>y,所以 z z z y y y 的左儿子。
    4. 显然,此时将要插入的序列接到 z z z 的左儿子(∅)上即可。

ins

c. 删除
  • 删除一段区间 [ l , r ] [l,r] [l,r]
    1. 分别找到 l l l 的前驱 p p p r r r 的后继 q q q
    2. p p p 转到根节点。
    3. q q q 转到 p p p 下方。由于 p < q p<q p<q,所以 q q q p p p 的左儿子。
    4. 显然,要删除的区间就是点 p p p 的整个左子树。直接变没即可。

信息的维护

以模板题 AcWing 2437. Splay 为例。

本题要求我们进行区间翻转操作。

因此维护两个值:

  1. 以每个点为根节点的子树的大小 size
  2. 区间翻转懒标记 flag

和线段树一样,两个函数 pushuppushdown 分别维护 sizeflag

本题的 Splay 保证中序遍历是当前序列的顺序,不一定满足 BST 性质。


例题

AcWing 2437. Splay

原题链接

本题仅是插入和翻转两个操作。翻转就是把这个区间所在子树的左右儿子分别翻转。

具体细节看代码。

struct Splay_Node
{int s[2], p; // 左右儿子、父节点int v, size, flag; // 值、子树大小、懒标void init(int _v, int _p) // 初始化{v = _v, p = _p;size = 1;}
}tr[N];int n, m;
int root, idx;void pushup(int u) // 更新当前节点大小
{tr[u].size = tr[tr[u].s[0]].size + tr[tr[u].s[1]].size + 1;
}void pushdown(int u) // 将懒标记下传
{if (tr[u].flag) // 如果当前节点有懒标{swap(tr[u].s[0], tr[u].s[1]); // 就交换左右儿子tr[tr[u].s[0]].flag ^= 1; // 左儿子懒标记更新tr[tr[u].s[1]].flag ^= 1; // 右儿子懒标记更新tr[u].flag ^= 1; // 当前节点懒标记清空}
}void rotate(int x) // 旋转
{int y = tr[x].p, z = tr[y].p; // 当前节点的父亲和祖父int k = tr[y].s[1] == x, kk = tr[z].s[1] == y; // 0 -> left | 1 -> right// 这里一个小技巧判断哪个儿子tr[z].s[kk] = x, tr[x].p = z; // z的儿子改为xtr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y; // y的儿子改为x的反儿子tr[x].s[k ^ 1] = y, tr[y].p = x; // x的反儿子变成ypushup(y), pushup(x); // 更新节点x,y
}void splay(int x, int k)
{while (tr[x].p != k) // 如果k不是当前节点的父节点{int y = tr[x].p, z = tr[y].p; // 当前节点的父亲和祖父if (z != k) // 如果k不是当前节点的祖父if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x); // 不成链先转xelse rotate(y); // 成链先转yrotate(x); // 最后都转一遍x}if (!k) root = x; // 如果当前x是根结点就更新root
}void insert(int v) // 插入
{int u = root, p = 0; // 当前节点和其父节点编号while (u) p = u, u = tr[u].s[v > tr[u].v]; // 只要节点存在就往下找// 后面那句意思是如果插入的值比当前节点小就去左子树,否则去右子树u = ++ idx; // 动态开点编号if (p) tr[p].s[v > tr[p].v] = u; // 如果u不是根结点就更新p的儿子为utr[u].init(v, p); // 初始化新的点usplay(u, 0); // 将u整到根结点
}int kth(int k) // 找第k小数
{int u = root; // 从根结点开始找while (tr[u].size >= k){pushdown(u); // 找之前先下传懒标记if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0]; // 如果k比左子树小的话就去左子树else if (tr[tr[u].s[0]].size + 1 == k) return splay(u, 0), u; // 如果刚好在当前点就返回else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1]; // 否则去右子树}return -1; // 找不到就返回-1
}void output(int u) // 输出中序遍历"左-根-右"
{pushdown(u); // 访问之前先下传if (tr[u].s[0]) output(tr[u].s[0]); // 如果左子树存在就遍历左子树if (tr[u].v >= 1 && tr[u].v <= n) printf("%d ", tr[u].v); // 根结点不是哨兵就输出根结点if (tr[u].s[1]) output(tr[u].s[1]); // 如果右子树存在就遍历右子树
}int main()
{scanf("%d%d", &n, &m);for (int i = 0; i <= n + 1; i ++ ) // 多加2哨兵防止越界insert(i);int l, r;while (m -- ){scanf("%d%d", &l, &r);l = kth(l), r = kth(r + 2); // 由于前面加了一个哨兵所以如果我们想要提取区间[l,r]就要以l和r+2分割splay(l, 0), splay(r, l); // 将l转到根节点,将r+2转到根节点下方tr[tr[r].s[0]].flag ^= 1; // 把r+2的左子树打上懒标记}output(root);return 0;
}

P3369 【模板】普通平衡树

原题链接

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 x x x
  2. 删除 x x x 数(若有多个相同的数,应只删除一个)
  3. 查询 x x x 数的排名(排名定义为比当前数小的数的个数 + 1 +1 +1 )
  4. 查询排名为 x x x 的数
  5. x x x 的前驱(前驱定义为小于 x x x,且最大的数)
  6. x x x 的后继(后继定义为大于 x x x,且最小的数)
  • 插入:根据 BST 的性质,找到这个值所在的节点。如果该节点存在,则将 cnt + 1 \text{cnt}+1 cnt+1。如果不存在就新建一个节点。
  • 删除:找到这个值的前驱 prev \text{prev} prev 和后继 next \text{next} next(节点编号),将 prev \text{prev} prev 转到根节点,将 next \text{next} next 转到 prev \text{prev} prev 下方。如果 next \text{next} next 左儿子 cnt > 1 \text{cnt}>1 cnt>1 则将 cnt − 1 \text{cnt}-1 cnt1,否则直接删除左儿子。
  • 根据数值找排名:将该数值对应的节点转到根节点,然后返回左子树的大小 + 1 +1 +1
  • 根据排名找数值:从根结点开始找,如果 k k k 比左子树小的话就去左子树,如果刚好在当前点就把这个点转上去并返回,否则去右子树。
  • 求前驱:根据 BST 性质先找出它的位置转到根节点。如果这个值不存在即根节点值小于输入值,则返回根节点值。否则返回根结点左子树的最右儿子。
  • 求后继:根据 BST 性质先找出它的位置转到根节点。如果这个值不存在即根节点值大于输入值,则返回根节点值。否则返回根结点右子树的最左儿子。
struct Node
{int size, cnt, v;int p, s[2];void init(int _v, int _p){v = _v, p = _p;size = 1;}
}tr[N];int n;
int root, idx;void pushup(int x) // 更新子树大小
{tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + tr[x].cnt; // 注意因为值可以重复,所以加cnt
}void rotate(int x) // 旋转
{int y = tr[x].p, z = tr[y].p;int k = tr[y].s[1] == x;tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;tr[x].s[k ^ 1] = y, tr[y].p = x;pushup(y), pushup(x);
}void splay(int x, int k) // 这个可以去翻上面的注释
{while (tr[x].p != k){int y = tr[x].p, z = tr[y].p;if (z != k)if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);else rotate(y);rotate(x);}if (!k) root = x;
}void upper(int v) // 根据BST性质找值v所在的节点并转到根节点
{int u = root; // 从根结点开始while (tr[u].s[v > tr[u].v] && tr[u].v != v) // 如果值比当前节点小就去左子树,否则去右子树u = tr[u].s[v > tr[u].v];splay(u, 0); // 找到之后将这个节点转到根节点// 如果这个值不存在,则显然会返回这个值前驱或后继所在的节点
}int get_prev(int v) // 找前驱
{upper(v); // 转到根节点if (tr[root].v < v) return root; // 如果这个值不存在且根结点值小则根节点就是前驱int u = tr[root].s[0]; // 从左子树开始搜while (tr[u].s[1]) u = tr[u].s[1]; // 左子树最右面return u;
}int get_next(int v) // 找后继
{upper(v); // 转到根节点if (tr[root].v > v) return root; // 如果这个值不存在且根结点值大则根结点就是后继int u = tr[root].s[1]; // 从右子树开始搜while (tr[u].s[0]) u = tr[u].s[0]; // 右子树最左面return u;
}int get_rank_by_val(int v) // 根据数值找排名
{upper(v); // 转到根节点return tr[tr[root].s[0]].size + 1; // 左子树大小+1
}int get_val_by_rank(int k) // 根据排名找数值
{int u = root;while (tr[u].size >= k){if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];else if (tr[tr[u].s[0]].size + tr[u].cnt >= k) return splay(u, 0), tr[u].v; // 记得把当前点转上去else k -= tr[tr[u].s[0]].size + tr[u].cnt, u = tr[u].s[1];}return -1;
}void insert(int v) // 插入一个值
{int u = root, p = 0;while (u && tr[u].v != v) p = u, u = tr[u].s[v > tr[u].v];if (u) tr[u].cnt ++ ; // 如果这个点已经存在就把cnt+1else{u = ++ idx; // 否则新建一个点if (p) tr[p].s[v > tr[p].v] = u; // 如果新建的不是根结点就更新其父节点的儿子指针tr[u] = {1, 1, v, p}; // 初始化}splay(u, 0);
}void remove(int v) // 移除一个值
{int prev = get_prev(v), next = get_next(v); // 找出前驱和后继splay(prev, 0), splay(next, prev); // 将前驱转到根节点,将后继转到前驱下方int w = tr[next].s[0]; // 后继的左儿子是要删除的值if (tr[w].cnt > 1) tr[w].cnt -- , splay(w, 0); // 如果不止一个就把cnt-1然后转上去else tr[next].s[0] = 0, splay(next, 0); // 否则把next左儿子指针置空然后把后继转上去
}void output(int u)
{if (tr[u].s[0]) output(tr[u].s[0]);if (tr[u].v != -INF && tr[u].v != INF) printf("%d ", tr[u].v);if (tr[u].s[1]) output(tr[u].s[1]);
}int main()
{int op, x;insert(INF), insert(-INF); // 为防止出界整两个哨兵scanf("%d", &n);while (n -- ){scanf("%d%d", &op, &x);switch (op){case 1: insert(x); break;case 2: remove(x); break;case 3: printf("%d\n", get_rank_by_val(x) - 1); break; // 由于有哨兵,所以排名-1case 4: printf("%d\n", get_val_by_rank(x + 1)); break; // 由于有哨兵,所以输入+1case 5: printf("%d\n", tr[get_prev(x)].v); break; // 由于找前驱返回的是下标case 6: printf("%d\n", tr[get_next(x)].v); break; // 所以输出数值}}return 0;
}

最后,如果觉得对您有帮助的话,点个赞再走吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/89099.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

0基础学习VR全景平台篇 第101篇:企业版功能-子账号分配管理

大家好&#xff0c;欢迎观看蛙色VR官方系列——后台使用课程&#xff01; 本期为大家带来蛙色VR平台&#xff0c;企业版教程-子账号分配管理功能&#xff01; 功能位置示意 一、本功能将用在哪里&#xff1f; 子账号分配管理功能&#xff0c;主要用于企业版用户为自己服务的终…

大模型助力企业数据驱动,火山引擎数智平台发布AI助手

9月19日&#xff0c;火山引擎在其举办的“V-Tech数据驱动科技峰会”上宣布&#xff0c;火山引擎数智平台VeDI推出“AI助手”&#xff0c;通过接入人工智能大模型&#xff0c;帮助企业提升数据处理和查询分析的效率。即使是不会写代码的运营人员&#xff0c;和大模型对话也能做好…

Java内存泄漏知识(软引用、弱引用等)

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 未经允许不得转载 目录 一、导读二、概览三、相关知识3.1 内存…

前后端分离的计算机毕设之基于springboot+vue的课程设计选题管理系统(内含源码+文档+教程)

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业毕业设计项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ &#x1f345;由于篇幅限制&#xff0c;想要获取完整文章或者源码&#xff0c;或者代做&am…

唤醒手腕 2023年 B 站课程 Golang 语言详细教程笔记(更新中)

0001、1000集GO语言Flag毒誓 唤醒手腕UP猪Pig目标花费1000集进行讲解Go语言视频学习教程&#xff08;有趣的灵魂&#xff0c;适合小白&#xff0c;不适合巨佬&#xff09;&#xff0c;从2023年3月19日开始&#xff0c;将会一直每天更新&#xff0c;准备在2024年5月1日之前更新…

Pikachu靶场——SSRF 服务端请求伪造

文章目录 1 SSRF 服务端请求伪造1.1 SSRF(curl)1.1.1 漏洞防御 1.2 SSRF(file_get_content)1.2.1 漏洞防御1.2.3 SSRF 防御 1 SSRF 服务端请求伪造 SSRF(Server-Side Request Forgery:服务器端请求伪造) 其形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能&a…

孤网双机并联逆变器下垂控制策略MATLAB仿真模型

微❤关注“电气仔推送”获得资料 主体模块&#xff1a; 建议使用MATLAB2021b及以上版本打开&#xff01; 功率计算模块、下垂控制模块、电压电流双环控制模块 系统输出有功功率: 系统输出无功功率&#xff1a; 系统频率变化曲线: 参考文献&#xff1a; 微电网并网运行模式下…

四川众佰诚:现在的抖音小店怎么注册

抖音小店是一种新的电子商务形式&#xff0c;它允许用户在抖音平台上直接销售商品。下面是一篇关于如何注册抖音小店的步骤说明&#xff1a; 下载并安装抖音App: 首先&#xff0c;你需要在手机上下载并安装最新版本的抖音App。你可以在苹果App Store或者安卓应用商店找到它。 创…

助力交叉学科应用型数据科学人才培养,和鲸科技携手华为发布联合解决方案

时代高速发展&#xff0c;智能化的浪潮奔腾而来&#xff0c;以“加速行业智能化”为主题&#xff0c;第八届华为全联接大会&#xff08;HUAWEI CONNECT 2023&#xff09;于 9 月 20 日正式开幕。本次大会中&#xff0c;华为携手生态伙伴引领智慧教育新风尚&#xff0c;和鲸科技…

飞书与企业微信的异同

云文档 飞书的云文档会自动用游览器打开&#xff0c;不会直接在PC应用中打开&#xff08;移动端能在应用中打开&#xff09;。 飞书云文档能够插入视频、流程图、问卷等等 聊天消息交互 钉钉也有类似的功能&#xff0c;可以针对消息进行点赞等回复 钉钉的消息回复还有【收到…

第2讲:Vue开发环境的搭建及运行

Vue开发环境搭建步骤 1、安装node http://www.nodejs.com.cn/ 一般安装在根目录下&#xff0c;直接下一步下一步安装即可。如何检测安装完毕 node -v 2、第二步&#xff1a;安装vue-cli脚手架 npm install -g vue/cli &#xff0c;查看安装版本 vue --version 3、第…

C++常见设计模式

设计模式 设计模式的六大原则&#xff1a;https://zhuanlan.zhihu.com/p/110130347 适配器模式 假设Client想要一个正方形&#xff08;client Interface&#xff09;&#xff0c;但是提供的服务&#xff08;service&#xff09;是个圆形&#xff0c;那么我就把这个圆通过适配…

java-decompiler

Java Decompiler GitHub F:\Document_JD-GUI\jd-gui-windows-1.4.0 jd-gui-windows-1.4.0 JDK 1.7 jd-gui-windows-1.6.6 JDK 1.8 Releases java-decompiler/jd-gui GitHub

flutter web 优化和flutter_admin_template

文章目录 Flutter Admin TemplateLive demo: https://githubityu.github.io/live_flutter_adminWeb 优化 Setup登录注册英文 亮色主题 中文 暗黑主题管理员登录权限 根据权限动态添加路由 第三方依赖License最后参考学习 Flutter Admin Template Responsive web with light/da…

【操作系统笔记十四】科普:POSIX 是什么

注&#xff1a;本文转载自该文章posix是什么都不知道&#xff0c;还好意思说你懂Linux&#xff1f; Linux开发者越来越多&#xff0c;但是仍然有很多人整不明白POSIX是什么。本文就带着大家来了解一下到底什么是POSIX&#xff0c;了解他的历史和重要性。 一、什么是 POSIX&…

虹科分享 | 网络保险:有效承保网络风险解决方案

文章来源&#xff1a;虹科网络安全 点击阅读原文&#xff1a;https://mp.weixin.qq.com/s/myCFPYtVVz5TPSFQaKqvLg 网络风险似乎往往很难量化&#xff0c;这使得保险公司很难适当地承保其网络风险政策。威胁载体的数量和不断发展的威胁&#xff0c;如新型恶意软件/勒索软件&…

RFID技术:钢条加工现场的智能化管理利器

RFID技术&#xff1a;钢条加工现场的智能化管理利器 RFID&#xff08;Radio Frequency Identification&#xff09;技术作为一种非接触式自动识别技术&#xff0c;近年来在工业领域得到广泛应用。本文将探讨RFID在钢条加工现场的应用&#xff0c;包括材料追踪与管理、生产过程…

安卓系列机型 框架LSP 安装步骤 支持多机型 LSP框架通用安装步骤【二】

​​​​​​安卓玩机教程---全机型安卓4----安卓12 框架xp edx lsp安装方法【一】 低版本可以参考上个博文了解相关安装方法。 LSP框架优点 简单来说装lsp框架的优点在于可以安装各种模块。包括 但不限于系统优化 加速 游戏开挂等等的模块。大致相当于电脑的扩展油猴 Lspos…

SAP服务器文件管理

SAP服务器文件管理 文件说明&#xff1a;对于SAP服务器的文件管理&#xff0c;系统给出3个事物码&#xff0c;分别是显示目录的AL11&#xff0c;下载文件的 CG3Y和上传文件的CG3Z。 AL11显示目录:以查找系统参数文件为例&#xff0c;在前台执行事物码AL11进入&#xff0c;如图…

word中使用latex多行公式,矩阵公式

\eqarray{H& [h(x_1)^T,\cdots,h(x_N)^T]^T \\ & [\matrix{g(w_1 x_1b_1) & \cdots & g(w_L x_1b_L) \\ \vdots & \ddots & \vdots \\ g(w_1 x_Nb_1) & \cdots & g(w_L x_Nb_L)}]_{N \times L}}&的引起的那条竖线可以通过backspace或者del…