《浅谈亚 log 数据结构在 OI 中的应用》 - 学习笔记

《浅谈亚 log 数据结构在 OI 中的应用》 - 学习笔记

向 $ 哥哥学习!

需要解决的问题:插入、删除、前驱、后继。不需要考虑相同元素。

2 压位 trie

平衡树和树状数组都没什么优化空间,把它们丢进垃圾堆里。

考虑 trie 有没有什么操作。此时想起来 trie 似乎并不只能是二叉。

但是多叉有一个大问题:询问的时候,如果子树中没有合法值,那就要在其他儿子里找最大/最小值。也就是要在儿子集合里寻找前驱后继。

也就是说,对于一个 \(w\) 叉树,需要支持大小为 \(w\) 的集合的快速插入删除前驱后继。

比如 \(w=64\) ,那就可以通过二进制压位来维护这个集合。前驱后继都可以使用神奇的二进制操作 \(O(1)\) 实现。

于是复杂度 \(O(\log_w V)\) ,其中 \(V\) 是值域。

假装值域不是太大,那么各种操作可以自底向上实现,带来各种剪枝,并且常数也比递归小很多。

当然如果值域太大那就只能动态开点了吧。

空间复杂度其实是 \(O(V/w)\) ,因为大小不超过 \(w\) 的时候就只需要 \(O(1)\) 个整形了。但是如果动态开点就需要记录儿子编号,就变回 \(O(V)\) 了。

拓展

插入、求 rank 。删除可以当做是插到另外一棵树里。

此时的大问题是不能快速求前 \(k\) 个子树的元素个数。

设叉数是 \(B\) 。如果每次插入都重新算一遍前缀和,那就 \(O(B\log_B V)\) 了,非常垃圾。

修改和询问均衡一下,变成一个节点插入 \(B\) 次之后再重构,那么插入的复杂度均摊下来就没有问题。问题在于 \(B\) 个零散元素怎么 \(O(1)\) 查询。

仍然压位。用 1 的个数表示某棵子树内的零散元素个数,不同子树之间用 0 隔开。那么就只需要对一个二进制位查询第 \(k\) 个 0 的位置。可以预处理。为了让预处理时间不爆炸只能 \(B<\log n\)

(我觉得)更简单的做法:直接令 \(B=\sqrt{64}\) ,把 \(B^2\) 个位置均匀分给 \(B\) 个子树,然后插入的时候直接填在对应位置即可。

复杂度是单次操作均摊 \(O(\log_B V)\) ,如果取 \(B=O(\log n)\) 就是 \(O({\log V\over \log\log n})\)

3 vEB tree

压位 trie 的瓶颈在于需要用二进制操作维护儿子集合,所以叉数不能太大。

但是发现对儿子集合的询问也不外乎插入删除、前驱后继,这提示我们可以套娃。

对于一个大小为 \(2^k\) 的 vEB tree ,令 \(m=k/2\) ,那么就分出 \(2^{k-m}\) 个子树。另外再开一个大小为 \(2^m\) 的 vEB tree 维护所有儿子。

\(high(x),low(x)\) 表示 \(x\) 的高位低位;设 \(A_y\) 表示 \(y\) 对应的子树;设 \(B\) 表示维护儿子的树。

树中维护 \(min,max\) ,表示集合的最大最小值。

较为特殊的一点:我们不把 \(min\) 插入到子树中,仅仅放在根节点的位置。这是为了保证复杂度。

插入

如果 \(x<min\) 那么 swap 一下。

如果对应儿子已经存在,那么 \(B\) 中就不需要修改,直接给儿子插入即可。

否则新建对应儿子,然后把这个儿子插入到 \(B\) 里。儿子里只有一个元素所以不需要递归。

删除

如果 \(x=min\) 那么只需要更新新的 \(min\) 。在 \(B\) 和对应的 \(A\) 分别拿出 \(min\) 即可。然后变成在 \(B\) 和对应的 \(A\) 里删除新的 \(min\)

如果 \(x\) 对应的 \(A\) 大小为 1 那么直接删掉,然后在 \(B\) 里面删除;否则 \(B\) 不需要修改,在 \(A\) 里面删除。

前驱后继

如果 \(x\) 对应的 \(A\) 里面有合法元素那么直接递归。否则在 \(B\) 里面找到前驱/后继,然后直接把 min/max 拿走。

注意特判没有插入的 \(min\)

简单优化

当大小不超过 \(64\) 时直接用位运算干掉。极大地减小递归深度。

复杂度

任何操作都只会递归一边,每次递归会让大小取根号,所以复杂度 \(O(\log\log V)\) 。但是由于大部分情况 \(V\approx 2^{24}\) ,而在大小为 \(2^6\) 时就已经可以不用再递归了,所以递归次数极少。

空间复杂度不太会分析也懒得分析,论文说是 \(O({2^k\over \sqrt w})\)

4 例题

第一题的压位 trie 用法非常 trivial ,重点看第二题的树上压位 trie 合并。

【ZJOI 2019】语言

原做法是维护一些点的虚树大小,要支持合并两个子树对应的虚树。无脑做法就是线段树合并。

我们考虑压位 trie 能否支持合并。

因为现在是动态开点压位 trie ,所以要维护所有子节点的编号。合并的时候需要把其中一棵树的儿子编号拉到另外一棵树去,但是暴力拉就给复杂度乘了 \(w\) ,不太行。

一个儿子至少对应一个节点,所以用启发式合并即可。

每拉一个子树之后还要记得更新相邻元素的距离和。

这时候又发现空间是 \(O(nw\log_w n)\) 有点爆炸。

重链剖分,每次直接把重儿子的 trie 拉过来。这样任何时刻只有 \(O(\log n)\) 个压位 trie ,每个 trie 只有 \(n/w\) 个点,空间复杂度变成 \(O(n\log n)\)

常数较小?不是很懂。

posted @ 2021-06-29 20:30  p_b_p_b  阅读(1463)  评论(0编辑  收藏  举报