数据结构(二)
桶的惰性清空
在加入桶时附带这次的时间标记。
如果发现此时的标记和这个桶上的标记不同,则清除桶的内容。
常数会减小。类比可以得到数据结构清空方式。
历史和问题
方法一:设计时间标记,每次访问一个节点后,把缺少的这段时间的贡献加上,然后让这个节点的时间和全局同步。
方法二:设计时间流逝标记,每次时间流逝时,在全局打一个标记,时间流逝标记要支持下传。
相对来讲第二种方法更为无脑,但是第一种方法更通用一些。
Segment Tree Beats
吉司机线段树。区间对 \(x\) 取 \(\max\),区间和。
记录线段树节点的最小值 \(mn\),次小值 \(se\),分类讨论:
- 若 \(x\le mn\),直接结束递归。
- 若 \(mn<x\le se\),只更改 \(mn\) 一个数后结束递归。
- 若 \(se<x\),继续递归。
如果只有这两个操作时间复杂度就是 \(O(n\log n)\) 的,证明就是向下递归对应一次值域合并。
如果多一些奇怪的操作,时间复杂度可能会变化。比如加上区间加以后会变成 \(O(n\log^2 n)\),怎么卡在 APIO 课件里。
可持久化
每次修改时,新建一个版本代表变化。每次查询版本时,尽可能借用其他版本的信息。
其实一切东西也就只是可持久化数组而已。但是注意势能分析的复杂度是不可以平凡可持久化的,比如 LCT。
当然可以改可持久化方式,不过没啥应用场景。
稍微比较有意思的就是可持久化单调栈,但是没什么人提到这个数据结构。
插入-标记-回收
用于离线解决区间 \([l,r]\) 的函数复合问题。
在 \(l\) 处插入 \(x\),在每个位置对目前所有的 \(x\) 做统一变换,然后再在 \(r\) 处收回。是一个很平凡的东西。
例题:澡堂。
FHQ-Treap
每个点有随机权值 \(w_i\),要求键值 \(v_i\) 满足二叉搜索树的限制,\(w_i\) 满足堆的限制。
FHQ-Treap 基于分裂。分裂时只需要按照键值把一整颗树分开,合并时根据 \(w_i\) 合并即可。
FHQ-Treap 可以打标记,分裂时先下传标记再分裂,合并时先把被合并节点下传标记再合并。分裂以后需要重新 pushup,合并也需要一次 pushup,讨论一下就好了。
有交并
如果没有奇怪的分裂操作,那么有交并和线段树合并一样,是自然的,怎么写都没啥问题。
如果有复制或者复用操作,那基本就别想对了,应该很好卡。
否则,考虑按照两棵树组成的连续段数为复杂度合并。
把合并后的所有键值排序后得到:\(\mathtt{ABBABBAAA}\) 有 \(5\) 个连续段,其中 \(\mathtt{A}\) 表示这个键值来自第一棵树。需要处理键值相同的情况,这点我们稍后再做讨论。
额,我本来想写个证明,但是我有点懒,先鸽了。
总之算法就是把优先级较大的根作为基准,然后把另一棵树按照这个基准分裂,然后两侧分别合并到左子树和右子树上。如果存在和基准相同的数,要用并查集把这个点和根合并到一起。
例题:TEST_176。
莫队
按 \(B\) 分块后,把询问按 \(l\) 所在的块编号为第一关键字,按 \(r\) 为第二关键字排序。
根据排序结果,移动指针以达到这次的 \((l,r)\)。这要求你可以通过 \([l,r]\) 的答案 \(O(1)\) 推到 \([l,r+1],[l-1,r],[l,r-1],[l+1,r]\) 这四个位置的答案。
第一,不一定要是区间才能使用这个技巧。实际上只要是若干个指针就可以。更深刻的,其实是在高维空间里的游走:每次走到相邻的整点,要求以某个顺序遍历所有点。
回滚莫队是只允许加入和撤销的莫队,撤销这个操作其实很弱,只要不是均摊就可以。
线段树分治
cxl 论文:线段树分治是时间维上的区间对偶范围的不删除莫队。
当然我们可以尝试着懂一下这个东西。
不删除莫队:只使用加入和撤销操作维护一个集合。
对偶范围:\(m\) 种修改的范围是 \(S_1,S_2,...,S_m\),\(n\) 个点中第 \(i\) 个点的对偶范围为 \(\{j|i\in S_j\}\)。也就是包含这个点的修改的集合。
线段树分治处理的问题离线后,问题变成:在时间区间 \([tl,tr]\) 中添加元素 \(x\)。
解决的 \(T\) 个问题都是区间对偶范围内的所有元素的问题。并且不允许使用删除。
我没看懂原型 RT 树,如果看懂了就放“数据结构(一)”里。
ST 表
常用的 ST 表有两种:第一种用于查询,第二种用于修改。
要求运算有 \(x\oplus x=x\) 的性质,所以应用范围比较窄。
分别可以做区间取 \(\min\),求最后序列和给定序列,查询区间 \(\min\) 两种问题。
复杂度 \(O(q+n\log n)\)。
颜色段均摊
用于解决区间赋值:赋值以后整个区间的颜色变成相同的,有势能。
写一个 set,set 上每个点表示当前颜色段的左端点。赋值时可能会加入 \(l,r+1\) 两个点,并删除 \([l,r]\) 内所有点。注意,你需要把相同的颜色合并。
可以用 mutable 这样的技巧,也可以用指针指向可修改的元素或数组。
整体加响应到局部加
设 \(\mathrm{ltag}_u\) 表示应有的整体加,设 \(\mathrm{utag}_v\) 表示已经被响应的局部加,每次对 \(v\) 做 pushdown,则响应 \(\mathrm{ltag}_u-\mathrm{utag}_v\) 的局部加,\(\mathrm{utag}_v=\mathrm{ltag_u}\)。
和桶的惰性清空本质上是一样的。
虚树
虚树。
有两种构建方式,二次排序法和单调栈法。
点分治
点分治分为四个步骤:
- 找到当前连通块的重心。在得知连通块大小的前提下,需要跑一遍 dfs。否则需要跑两遍。
- 从重心开始 dfs,并求出 dfn 和 siz。
- 使用 dfn 完成后续操作。
- 删去重心,用 siz 得知每个分出来的连通块的大小,递归。
点分树
对于特定点到根的查询,dfn 不好存。查询至少要 log。
好的方法:把每个分治中心 push_back 到它所在的连通块内的所有点即可。
树套树
离线版树套树:树状数组套树状数组,先把每个小树状数组里可能有值的位置找出来,离散化,查询和修改时二分,空间复杂度 \(O(n\log n)\)。
不推荐:树状数组套树状数组,并用 unordered_map 存值。unordered_map 的常数大到可以看作 \(O(\log n)\)。实际上手写哈希表也不推荐。
树状数组套平衡树最保险。
重链剖分
主要有两点:一个点到根的轻边数量是 \(O(\log n)\) 的,轻子树大小和是 \(O(n\log n)\) 的。
长链剖分
dp 状态和深度有关时可以考虑长链剖分。
所有长链长度的和为 \(n\),这是显然的,关键在于一条长链可以容纳几乎所有 dp 信息(大于子树高度的 dp 信息较少,通常为 \(0\) 或 \(1\))。
写法是 dfs 序。枚举一条长链时,从链顶的 dfs 序往后枚举即可。
小数据结构
小数据结构很好用。例子:点分树时,你要求到根 \(\le d\) 的信息,看成先做若干个单点加,再做若干个前缀查。
开一个数组,维护指针 \(l,r\) 表示这个小数据结构在数组上对应的区间,单点加时扩容,单点加后做一遍前缀和并更新当前占用的空间,前缀查时简单查。这样就写了一个简单但是功能清晰的小数据结构。
线性 RMQ
对序列按 \(w\) 分块,每块求出最大值,块内求出前缀最大值和后缀最大值。对于 \(n/w\) 个数建 ST 表。如果跨块就可以直接求出答案。问题在于块内。
对于一个块 \([l,r]\),块内对于每个 \(i\) 存一个 \(w\) 位整数表示考虑了 \(l\le x\le i\) 的所有数后建立的单调栈(单调递减)有哪些数在。查询时看 \(R\) 处的单调栈,在 \(L\) 右侧第一个 \(1\) 的位置。位运算即可。
如果不是 RMQ,比如是查询按位或,块内可以暴力,复杂度是 \(O(n\sqrt{\log n})\) 的,有一个 \(1/2\) 的常数。

浙公网安备 33010602011771号