【学习笔记】浅析平衡树套线段树 & 带插入区间K小值
常见的树套树
一般来说,在嵌套数据结构中,线段树多被作为外层结构使用。
但线段树毕竟是 静态 的结构,导致了一些不便。
下面是一个难以维护的例子:
带插入区间 \(k\) 小值问题
来源:Luogu P4278 & BZOJ 3065
给定一个初始长为 \(n\) 的正整数序列,执行 \(m\) 次操作:
Q x y k:查询区间 \([x, y]\) 中第 \(k\) 小的数的大小;M x val:将位置 \(x\) 的数字修改为 \(val\);I x val:在位置 \(x\) 前插入数字 \(val\)。
插入次数、原序列长度 \(\le 3.5\times 10^4\),查询次数、数值大小 \(\le 7\times 10^4\)。强制在线。
不常见的树套树
由于数列出现的结构上的变动,线段树作为静态数据结构已经难以维护。
于是我们考虑用动态结构的 平衡树 作为外层结构,即 平衡树套线段树。
但是毕竟为外层结构,我们选择的平衡树也不能太跳,这里暂且使用 替罪羊树。WBLT 之类的应该也行,不过这里暂且介绍一种 不会。
替罪羊树不同于 Splay,Treap——它不用旋转。为了维持平衡,我们会在不平衡的结点 排扁重构。是否足够不平衡是根据子树大小判断的。替罪羊树相对于旋转平衡树结构更稳定,更适合作为外层结构。
在这里,替罪羊树上的每一个结点都包含着一颗 动态开点权值线段树,表示以当前结点为根的子树中的数值构成的集合。
维护手法
建树
建树的整体框架与普通平衡树并无大异,只不过加上线段树部分即可。
大致来说就是当前线段树需要包含左右儿子以及自己的信息。
相对于一个个插入,使用 线段树合并 方法显然优秀一些。
我们知道线段树合并的总复杂度是一只 \(\log\) 的,所以总复杂度不会超过 \(O(n\log^2 n)\)。
操作
插入操作,我们只要进行常规的平衡树插入即可。平衡树上二分找到第 \(x\) 个位置前加入结点,同时在寻找的路径上的每一个平衡树结点上进行线段树的插入。这样的复杂度是 \(O(\log^2 n)\),因为平衡树深度为 \(O(\log n)\),所以一共更新了 \(O(\log n) \times O(\log n)\) 个线段树结点。在插入前需要判断重构。
单点修改也是同理,找到这个结点然后把这条路径进行线段树单点修改即可。复杂度仍然 \(O(\log^2 n)\)。
对于查询,我们回忆一下 Dynamic Rankings 的线段树(树状数组)套线段树做法,类似的我们也把这些平衡树子树“拎”出来,把零散的结点“抠”出来,然后一起二分即可。复杂度 \(O(\log^2 n)\)。
复杂度
时空复杂度均为 \(O(n\log^2 n)\)。
但是常数其实很大。用这个做法过掉上题的 Luogu 数据是非常困难的。
降低空间常数
如果一颗线段树的子树不代表任何元素,即 \(siz = 1\)。那么对于当前来说这个子树不要也罢,于是可以添加一个 垃圾回收机制。
代码实现
由于线段树不是实现重点,这里只展出替罪羊树的部分。
segt 前缀的是线段树;spat 前缀的是替罪羊树。
建树
int spatBuild(int l, int r) {
if (l > r) return 0;
int mid = (l + r) >> 1;
int x = createSpatNode(0, 0, a[mid], r - l + 1);
spat[x].lc = spatBuild(l, mid - 1);
spat[x].rc = spatBuild(mid + 1, r);
spat[x].seg_rt = segtMerge(
spat[spat[x].lc].seg_rt,
spat[spat[x].rc].seg_rt,
0, U);
segtInsert(spat[x].seg_rt, 0, U, a[mid], 1);
return x;
}
查询
int qry[N], qcnt;
int val[N], vcnt;
void spatScanOut(int x, int ql, int qr, int l, int r) {
if (ql > qr || l > r) return;
if (ql <= l && r <= qr) return qry[++qcnt] = x, void();
if (l > qr || r < ql) return;
int mid = spat[spat[x].lc].siz + l;
if (ql <= mid && mid <= qr) val[++vcnt] = spat[x].val;
spatScanOut(spat[x].lc, ql, qr, l, mid - 1);
spatScanOut(spat[x].rc, ql, qr, mid + 1, r);
}
int Query(int ql, int qr, int k) {
qcnt = vcnt = 0;
spatScanOut(spatRoot, ql, qr, 1, spat[spatRoot].siz);
int l = 0, r = U;
for (int i = 1; i <= qcnt; i++)
qry[i] = spat[qry[i]].seg_rt;
while (l < r) {
int mid = (l + r) >> 1;
int sum = 0;
for (int i = 1; i <= qcnt; i++)
sum += segt[segt[qry[i]].lc].siz;
for (int i = 1; i <= vcnt; i++)
sum += (val[i] <= mid);
if (k <= sum) {
r = mid;
for (int i = 1; i <= qcnt; i++)
qry[i] = segt[qry[i]].lc;
} else {
l = mid + 1, k -= sum;
for (int i = 1; i <= qcnt; i++)
qry[i] = segt[qry[i]].rc;
for (int i = 1; i <= vcnt; i++)
if (val[i] <= mid) val[i] = U + 5;
}
}
return l;
}
单点修改
int spatAt(int x, int k) {
while (x) {
if (spat[spat[x].lc].siz + 1 == k) return spat[x].val;
if (k <= spat[spat[x].lc].siz) x = spat[x].lc;
else k -= spat[spat[x].lc].siz + 1, x = spat[x].rc;
}
throw;
}
void spatEdit(int x, int k, int lst, int cur) {
segtInsert(spat[x].seg_rt, 0, U, lst, -1);
segtInsert(spat[x].seg_rt, 0, U, cur, 1);
if (spat[spat[x].lc].siz + 1 == k)
return spat[x].val = cur, void();
if (k <= spat[spat[x].lc].siz) spatEdit(spat[x].lc, k, lst, cur);
else spatEdit(spat[x].rc, k - spat[spat[x].lc].siz - 1, lst, cur);
}
void Replace(int pos, int val) {
int lst = spatAt(spatRoot, pos);
if (lst != val) spatEdit(spatRoot, pos, lst, val);
}
插入
void spatFlatten(int& x) {
if (!x) return;
spatFlatten(spat[x].lc);
a[++n] = spat[x].val;
spatFlatten(spat[x].rc);
destroySegtTree(spat[x].seg_rt);
p_rec[++p_top] = x, x = 0;
}
void spatRebuild(int& x) {
n = 0, spatFlatten(x);
x = spatBuild(1, n);
}
void spatInsert(int& x, int k, int val) {
if (!x) {
x = createSpatNode(0, 0, val, 1);
segtInsert(spat[x].seg_rt, 0, U, val, 1);
return;
}
++spat[x].siz, segtInsert(spat[x].seg_rt, 0, U, val, 1);
if (k <= spat[spat[x].lc].siz) spatInsert(spat[x].lc, k, val);
else spatInsert(spat[x].rc, k - spat[spat[x].lc].siz - 1, val);
if (checkBad(x)) spatRebuild(x);
}
void Insert(int pos, int val) {
spatInsert(spatRoot, pos - 1, val);
}
完整实现
Record:https://darkbzoj.tk/submission/83902
14695ms|117088kb|6.5kb
后记
- 原文地址:https://www.cnblogs.com/-Wallace-/p/13623410.html
- 本文作者:@-Wallace-
- 转载请附上出处。
【学习笔记】浅析平衡树套线段树 & 带插入区间K小值的更多相关文章
- BZOJ 3065 带插入区间K小值(sag套线段树)
3065: 带插入区间K小值 Time Limit: 60 Sec Memory Limit: 512 MBSubmit: 4696 Solved: 1527[Submit][Status][Di ...
- 3065: 带插入区间K小值_树套树_替罪羊树_权值线段树
经过周六一天,周一3个小时的晚自习,周二2个小时的疯狂debug,终于凭借自己切掉了这道树套树题. Code: #include <cstdio> #include <algorit ...
- 【题解】BZOJ 3065: 带插入区间K小值——替罪羊树套线段树
题目传送门 题解 orz vfk的题解 3065: 带插入区间K小值 系列题解 一 二 三 四 惨 一开始用了一种空间常数很大的方法,每次重构的时候merge两颗线段树,然后无限RE(其实是MLE). ...
- [BZOJ3065]带插入区间K小值 解题报告 替罪羊树+值域线段树
刚了一天的题终于切掉了,数据结构题的代码真**难调,这是我做过的第一道树套树题,做完后感觉对树套树都有阴影了......下面写一下做题记录. Portal Gun:[BZOJ3065]带插入区间k小值 ...
- 【BZOJ3065】带插入区间K小值 替罪羊树+权值线段树
[BZOJ3065]带插入区间K小值 Description 从前有n只跳蚤排成一行做早操,每只跳蚤都有自己的一个弹跳力a[i].跳蚤国王看着这些跳蚤国欣欣向荣的情景,感到非常高兴.这时跳蚤国王决定理 ...
- bzoj 3065: 带插入区间K小值 替罪羊树 && AC300
3065: 带插入区间K小值 Time Limit: 60 Sec Memory Limit: 512 MBSubmit: 1062 Solved: 253[Submit][Status] Des ...
- 【bzoj3065】: 带插入区间K小值 详解——替罪羊套函数式线段树
不得不说,做过最爽的树套树———— 由于有了区间操作,我们很容易把区间看成一棵平衡树,对他进行插入,那么外面一层就是平衡树了,这就与我们之前所见到的不同了.我们之前所见到的大多数是线段树套平衡树而此题 ...
- 【bzoj3065】带插入区间K小值 替罪羊树套权值线段树
题目描述 从前有n只跳蚤排成一行做早操,每只跳蚤都有自己的一个弹跳力a[i].跳蚤国王看着这些跳蚤国欣欣向荣的情景,感到非常高兴.这时跳蚤国王决定理性愉悦一下,查询区间k小值.他每次向它的随从伏特提出 ...
- 【BZOJ】3065: 带插入区间K小值
http://www.lydsy.com/JudgeOnline/problem.php?id=3065 题意:带插入.修改的区间k小值在线查询.(原序列n<=35000, 询问<=175 ...
随机推荐
- PyCharm离线安装PyQt5_tools(QtDesigner)
目录 下载所需的whl包 安装whl 配置PyCharm 测试 下载所需的whl包 打开链接 PyPI,依此搜索 python_dotenv,PyQt5_sip,PyQt5,pyqt5_tools:基 ...
- Java基础 之二 类和对象
1.基础概念 1) 概念 简单来说,类是具有相同特征事物的抽象,比如有轮子的都可以抽象为车:对象则可以看做类的具体实例,比如创建一个法拉利的车,就是车这个类的实例. 抽象.封装.继承.多态是类的四个特 ...
- 直面秋招!非科班生背水一战,最终拿下阿里等大厂offer!
前言 2020年已经接近到9月份了,很多粉丝朋友都对金九银十雀雀欲试了吧!也有很多朋友向我求教经验,因为我自己工作相对于稳定,在这里给大家分享一个粉丝朋友的经历,他作为一个曾经的菜鸡面试者,在不断的失 ...
- MyBatis学习02
3.增删改查实现 select select标签是mybatis中最常用的标签之一 select语句有很多属性可以详细配置每一条SQL语句 SQL语句返回值类型.[完整的类名或者别名] 传入SQL语句 ...
- u盘插电脑没反应的三大原因,以及解决方法
相信大家在使用U盘的过程中免不了会遇到这样的情况:u盘虽然与电脑连接,但是插上后却没有反应.很多小伙伴都摸不着头脑不知道到底是哪里出了错.其实大家也不用过于心急,只要找到了原因便可很快得到解决. u盘 ...
- 【基于PUPPETEER前端自动化框架】【一】TypeScript+Puppeteer+Jest 整合
前提:掌握Jest + Puppeteer 1.Jest环境配置 2.Jest-MATCHERS匹配器 3.Jest-全局变量设置 4.Puppeteer安装 5.Puppeteer元素获取 6.Pu ...
- vulnhub: DC 2
首先地址探测找到主机IP: root@kali:~# nmap -sn 192.168.74.139/24 Starting Nmap 7.80 ( https://nmap.org ) at 202 ...
- Contest 985
A 均移到黑色或白色即可. 时间复杂度 \(O\left(n\log n\right)\). B 枚举每种开关判断是否有灯只能靠该种开关控制. 时间复杂度 \(O\left(nm\right)\). ...
- Java蓝桥杯练习——杨辉三角形
问题描述: 杨辉三角形又称Pascal三角形,它的第i+1行是(a+b)i的展开式的系数. 它的一个重要性质是:三角形中的每个数字等于它两肩上的数字相加. 下面给出了杨辉三角形的前4行: 1 1 1 ...
- Docker基础知识及入门
什么是Docker? Docker是由dotcloud公司使用golang语言进行开发的,基于Linux内核的 cgroup,namespace,以及OverlayFS类的Union FS等技术,对进 ...