高级数据结构学习笔记
更多进阶内容见:网课-数据结构学习笔记2 & 洛谷noip金秋营
一、线段树
0. 普适技巧
-
动态开点:节省空间。
-
标记永久化:分块的块标记本质就是这个。可以节省空间。
-
多
tag
线段树:一定要注意tag
的先后关系,还要注意相互转化关系。例:P4314 CPU 监控、P3373 【模板】线段树 2 -
线段树二分:
-
如果是递归终止区间(该区间完全被包含在询问范围内),使用整区间信息判断是继续递归 / 还是返回。
-
如果已经是叶节点(
)说明找到目标位置,返回。 -
讨论询问区间是否与当前区间的左 / 右区间相交,判断并往下递归。
-
1. 区间最值 & 历史区间最值
2. 二维线段树
简单地说,就是线段树套线段树。对于第一棵线段树的每一个节点,都要单独开一棵线段树,非常消耗空间。故此,二维线段树一般用于一下两种情形:
-
数据范围很小。往往此时维护的信息也很简单,可以不用写
struct
建树,而只是用数组存储。 -
操作很少。可以动态开点,此时的空间复杂度为
。
标记永久化是二维线段树区间修改的一个重要技巧,也是使用二维线段树的题目的难点。
与其它二维数据结构的对比:
名称 | 信息类型 | 操作要求 | 时间复杂度 | 空间复杂度 |
---|---|---|---|---|
二维 ST 表 | 区间 | 静态 | ||
二维树状数组 | 前缀 | 动态 | ||
可持久化线段树 | 一维前缀 + 一维区间 | 静态 | ||
树状数组套线段树 (动态可持久化线段树) |
一维前缀 + 一维区间 | 动态 | ||
二维线段树 | 区间 | 动态 |
以下是一个类似的表,但它更侧重于解决二维数点问题:
算法 / 数据结构 | 在 / 离线 | 动 / 静态 | 修改 & 询问 | 时间复杂度 | 空间复杂度 |
---|---|---|---|---|---|
CDQ 分治 | 离线 | 动态 | 一维单修前缀查(若满足可减性,可以单修区查 / 区修单查),另一维区修区查 | ||
扫描线 | 离线 | 静态 | 同上 | ||
主席树 | 在线 | 静态 | 同上 | ||
树状数组套主席树 | 在线 | 动态 | 同上 | ||
二维线段树 | 在线 | 动态 | 两维区修区查(相比以前的优点是不满足可见性但可合并的信息都支持;但必须标记永久化,前提信息满足交换律) | ||
线段树套平衡树 | 在线 | 动态 | 单修区查 | ||
kD-Tree | 在线 | 动态 | 关键点可以单个插入删除,关键点信息支持区修区查 |
例题:
-
Luck and Love:单修区查
-
P3437 [POI2006] TET-Tetris 3D:区间查
区间取 。使用标记永久化。巧妙的地方在于,通过对一维情况的类推,想到通过在外层树的每个节点上开两棵内层树——一个表示区间 ,一个表 ——的方式来解决标记永久化。 -
P3688 [ZJOI2017] 树状数组:区修、单查,由于信息不满足可减性必须使用二维线段树。这道题告诉我们标记永久化能够维护的信息必须满足交换律。
-
P3157 [CQOI2011] 动态逆序对:这个题用动态开点可以搞过,但是建议用别的数据结构。
3. 可持久化线段树
可持久化的本质是对每一次操作的前缀和数组。而主席树则是一个数列上的权值前缀和数组,可用于解决区间权值问题。
可持久化线段树常常结合的一个技巧是线段树二分。比如,求
例题:
-
P2617 Dynamic Rankings & P3157 [CQOI2011] 动态逆序对 & P3759 [TJOI2017] 不勤劳的图书管理员:由于主席树的本质是前缀和,可以用树状数组实现动态修改。此时的代码已看不见可持久化的影子了。时间、空间复杂度
。 -
Sequence II、P4137 Rmq Problem / mex:我们惯常的主席树思路为以原区间下标为不同树根,以值域为线段树下标;但以什么为线段树下标,得看我们根本想要维护什么。如此题,线段树下标所代表的即为第一次出现位置。
-
To the moon:使用标记永久化以节省空间。
-
P3605 [USACO17JAN] Promotion Counting P、P8844 [传智杯 #4 初赛] 小卡与落叶:在树上进行但不是树链剖分的问题。以
序为下标的可持久化线段树,这种做法可在线维护子树信息。(同样能够维护子树信息的算法:线段树合并、树上启发式合并,不过它们都是离线的。) -
P2633 Count on a tree:与上面相对,这种则是维护从根到每个节点的可持久化线段树,结合树上差分,我们可以通过它维护路径信息。
4. 线段树分裂 & 合并
二、分块
1. 树上莫队
使用 欧拉序而非树链剖分的 dfs 序 将树转化为了序列,进而求解。
至于为什么不用——
口胡的结论:dfs 序适用于维护的信息可以由区间合并而来的情况,而欧拉序适用于维护的信息可以由区间加减相邻元素而调整的情况。(欢迎打脸)
—— 引自 @VTloBong 的博客
个人认为,欧拉序具有遍历的连续性,适用于莫队的更新方法;而 dfs 序的本质是欧拉序的简化版,可以很方便地应用在其他无此诉求的数据结构上。
实现上需要注意的是添加 lca
。
例题:
-
P4689 [Ynoi2016] 这是我自己的发明:重在拆解询问进行转化。这里将“根更改”视作“询问范围转换”是一个比较巧的 Trick。
2. 块状链表
由链表接起的一个个数组块,控制块长在
代码比较复杂,但常数较小。也许写熟练了会好一些。
三、平衡树
0. FHQ_Treap 及一些普适性内容
我之前已经写过一些内容了。这里想提一点。
平衡树实际有三个要素:键值、排名、下标。
其中下标是这个数插入时的一个不变量,有时固定的下标甚至是我们解题的突破口。
另外,在调试方面,对于基于随机的平衡树,换几个随机种子多试几次是很好的调试方法。
1. Splay
调试中常见问题:
-
0 节点
-
尽量使用循环不要使用递归
-
每次操作(不管是询问、插入还是删除)都要 splay(本质原因:势能分析法)
例题:P3369 【模板】普通平衡树、P4008 [NOI2003] 文本编辑器
2. 线段树套平衡树
与树状数组套主席树的做法对比:
时间 | 空间 | |
---|---|---|
树状数组套主席树 | ||
线段树套平衡树 |
注意事项:
-
空间:初始节点
和 个数 后面插入节点个数,即MAXN*MAXLOG+MAXN*8
。 -
二分的写法:二分找到最后一个连最大排名都小于 k 的值
-
FHQ_Treap 会被卡常。应使用 Splay。
3. 可持久化平衡树
为什么一般不使用 Splay 进行可持久化?
-
Splay 既有向上操作,又有向下操作。见:link。
-
Splay 复杂度为均摊。某一个版本可能访问复杂度极高,但数据逼迫你反复调用。
具体实现:
-
一般平衡树中只需要在 Split 中新建节点。
-
带有懒标记的平衡树要注意在下传时新建节点。因为懒标记的下传本质是对当前版本该节点的一个操作,与以前版本而共用该节点的无关。
-
注意在新建节点时不要建到空节点了。
例题:P3835 【模板】可持久化平衡树、P5055 【模板】可持久化文艺平衡树
4. k-D 树
一般用来处理二维及以上空间中的数点问题。
只有查询矩形(超长方体)时复杂度是真的,为
实现上需要注意:
-
KD 树是以“替罪羊树”为根基的平衡树。设置一个准值
Alpha
,若有子树大小与树大小比值超过Alpha
,则拍平重建这棵树。 -
询问时的“剪枝”:矩形完全不在该范围内的,返回;矩形完全在该范围内的,返回;矩形部分在该范围内的,判断当前点是否在范围内,然后向下递归。
-
节点要储存一个矩形的边框。故此,空节点要初始化为
INF
,以免Push_up
时出错。
例题:In case of failure、P4148 简单题、P4475 巧克力王国、P3769 [CH弱省胡策R2] TATT
5. Link-Cut Tree
LCT 是一种用于动态维护树的结构和树链信息的数据结构。
从树链剖分的思路出发。为了快速维护动态树,数据结构选用了可以分裂合并的平衡树。同时,splay 可进行区间翻转的性质刚好支持 Make_root 操作。
复杂度
大致过程:
-
Rotate(pt)
:左旋 / 右旋。 -
Splay(pt)
:先下放标记。然后重复如下内容:如果 fa(pt) 和 pt 是同种儿子(都是左 / 都是右),转 fa(pt);否则转 pt。再转一次 pt。 -
Access(x)
:当还不是原树的根时,不断Splay(x)
(提根),连接 x 与其虚父亲,然后对其虚父亲重复同样操作。 -
Make_root(x)
:Access(x)
后,将其所在区间整个翻转。
与一般树剖不同之处:
-
维护树的形态,而非值域。
-
链的不定性。
与一般 Splay 不同之处:
-
push_down 一定要预先从上到下进行。
-
“虚边”。这是一条单向边。可以这么认为:其指出是虚指,因为指出实际代表的是整条链的指出;而指到是实指,就是说被指到就是该节点本身被指到,而不是这条链被指到了。
实现上的易错点:
-
区分“原树”与“辅助树”上的操作。
-
Rotate 操作特判 father 是否为根。
-
push_down、push_up 处特判空节点。
-
当更新了某个节点的儿子时,一定要记得更新它儿子的父亲。
例题:
-
P4219 [BJOI2014] 大融合:如果要使用 LCT 维护子树信息,这个信息必须满足可减性。重点就是维护有关虚实转化的
Access
函数。 -
P4172 [WC2006] 水管局长:LCT 维护生成树。值得注意的是,我们通过“化边为点”的方式来在 LCT 上维护点权。
6. 全局平衡二叉树
全局平衡二叉树是一种用于维护静态树的数据结构。
准确地说,全局平衡二叉树应是一个二叉树森林。它在树剖的每一条重链上以
具体地,对于每一条重链上的二叉树,全局平衡二叉树按照每一个点的轻子树大小求出加权中点,当作树根。容易证明,这维护了整个森林(包括轻、重边)的总深度为
对于 链 修改、查询,全局平衡二叉树能以优秀的常数做到
-
点到根的路径
可以发现,在点
到根的路径上的点 ,满足在跳到过的链上且 。如何在一棵二叉搜索树上查找关键字更小的所有点呢?如果在中间某一步,从 向 上跳时,满足 为 的右儿子,那么 的左子树中的所有节点 都满足 。-
点到根路径查询:上跳,每次如果是从
的右儿子跳上来,就将 左儿子的信息累加入答案。 -
点到根路径修改:上跳,每次如果是从
的右儿子跳上来,就对 左儿子打上标记。(一般来说,全局平衡二叉树使用标记永久化;当然也可以用Push_down
,但这需要在预先知道上跳路径后再从上到下Push_down
一次。)
-
-
任意点到点之间的路径
先将二者的深度跳到一样,再维护两个指针同时向上直到跳到同一个点上就行了。过程和上面大部分是一样的,需要注意的是在同一条链上的情况,dfn 序偏左的节点需要在跳的过程中更新右子树而非左子树。
-
子树修改
维护轻儿子的信息与标记即可。此时的标记只能标记永久化。
所以全局平衡二叉树相比树剖劣在哪里呢?我一时真想不出来有什么树剖能做它做不了的。
子树信息的维护是全局平衡二叉树的短板。因为这时的标记只能使用标记永久化,有很多特殊的标记就维护不了。
例题:
四、堆
1. 左偏树
堆性质对左右节点的关系并无要求,致使堆的合并可以任意向左 / 右进行。因此,通过维护左偏,使得每个点向右到叶子节点的距离尽量小,可以得到一条尽量短的合并链。
注意事项:Merge、Delete 时注意维护并查集的变化。
例题:P3377 【模板】左偏树/可并堆、P1456 Monkey King
2. 启发式合并
将小的往大的合并。
显然,复杂度在两个堆的大小越接近时越劣。那么假设每次合并时大小都一样,可证明复杂度为
这是一个很有用的技巧,在很多其它数据结构上也可以使用。
五、并查集
0. 路径压缩 & 按秩合并
这么多年来终于看懂这两个的时间复杂度了。
路径压缩:时间复杂度是摊的
按秩合并:
-
修改, 查询(严格,难卡满)按照 深度 / 子树大小 合并时时间复杂度都是严格的
。单次的时间复杂度显然与树高(深度)挂钩。以深度为例:当加上按秩合并优化后,显然在树高相等时深度才会 。于是得证复杂度 。 -
均摊
修改, 查询不用并查集特色合并方式,而使用一般启发式合并的合并方式——将较小集合的元素依次插入较大集合中。优势在其很快的查询速率。
1. 可持久化并查集
其实就是利用可持久化线段树实现的可持久化数组,来实现并查集的 father 数组。
但是注意这里不能使用路径压缩,因为路径压缩的复杂度是均摊的,而可持久化不能用在均摊复杂度的数据结构上。
直接的在线做法时间复杂度
另外还有一个离线做法,不使用可持久化线段树,而是根据可持久化的继承关系建成一棵树。在树上进行 dfs,进入某子树时对其操作,退出时撤销(不用路径压缩时,并查集撤销可做到
六、离线分治算法
-
CDQ 分治
-
点分治
-
整体二分
七、扩展结构
1. 树链剖分
把树剖为链,满足性质:
-
dfn 在子树中连续
-
至多有
条链。
在每条链上独立地建立数据结构。以多一个
树链剖分一般用于路径查询,它也可以进行子树查询。值得一提的是,当只维护子树信息时,我们往往可以通过以 dfn
为下标的数据结构解决。
-
P2486 [SDOI2011] 染色、P3976 [TJOI2015] 旅游:这种题将需要维护的信息封为一个结构体将简化程序。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示