线性数据结构初探:加删查最小众数
线性数据结构初探:加删查最小众数
问题形式:有一个集合,你可以向其中加入一个数、删除一个数、查询出现次数最多的数中最小的那个。
值域和询问次数是线性,修改次数 \(n\sqrt n\),要求空间线性,最好能禁止哈希表。
注意:不保证任意时刻集合大小 \(\le n\)。
Part1
首先我们要会加删查众数出现次数,这个其实可以 \(\mathcal O(1)-\mathcal O(1)\),很简单的我们维护桶和“桶的权值桶”,就可以简单维护。
我们可以利用这个技巧做到不回滚的莫队区间众数。
Part2
接下来我们进行一个弱化版的问题,加删查最小众数线性空间,值域和询问次数是线性,修改次数 \(n\sqrt n\)。
但是保证任意时刻集合大小 \(\le n\)。
第二个条件是显著有用的,这给我们带来了一个自然根号——集合中数的种类数是 \(\mathcal O(\sqrt n)\) 的。
进行完 P1 后,我们有一个直接的想法是套一个 \(\mathcal O(1)-\mathcal O(\sqrt n)\) 的值域分块求最小值,这个东西的空间由两部分构成,一部分 \(\sqrt n\) 是块个数,存放每个块的出现次数,一部分 \(n\) 是值域数组,存放每个值的出现次数。
这个东西最大的问题是 对每个出现次数都要开一个这样的数据结构,空间复杂度甚至是平方的。
平凡地,对其哈希表可以得到一个线性空间的做法,但这显然不是我们想要的小常数线性空间。
@nalemy 提出了一个关键优化:所有该结构的“值域数组”是没必要的,我们已经维护了权值桶,在定位到最小值后直接扫描权值桶上的该块即可完成求解,我们降到了每个数据结构 \(\mathcal O(\sqrt n)\) 的空间复杂度。
结合上面的自然根号,@神天朗星 提出了一种线性空间的小常数做法。
我们提前开好 \(\mathcal O(\sqrt n)\) 个数据结构,分配给所有的出现次数,我们默认它可以 \(\mathcal O(1)\) 移动(移动其指针即可)。
把所有未启用的数据结构放到一个栈中,当有新的出现次数出现时,用栈给它分配一个,当有出现次数消失时,此时该数据结构必定 回到了空白状态,所以无需清空直接入栈。
@scallion 提出了一种不需要倒腾的空间线性做法,对 \(\le \sqrt n\) 的所有出现次数值域分块,\(>\sqrt n\) 的出现次数中元素个数是 \(\le \sqrt n\) 的,我们只需要支持 \(\mathcal O(1)\) 加删 \(\mathcal O(size)\) 遍历,对每个出现次数开链表,每种元素记录一下链表迭代器即可实现。
Part3
现在是最终问题:不保证任意时刻集合大小 \(\le n\)。
显著的变化是自然根号失效了,变成了 \(n^{0.75}\),但是值域是线性的,所以集合中的总个数是线性的,所以我们对值域分块做哈希表仍然是线性的,下文称这个做法为做法 1。
umap 做法仍然有一个转置做法,直接维护一个大的值域分块,每块维护 umap \(f_{j}\) 表示该块中 \(j\) 这个出现次数的出现次数,由于我们已知众数的出现次数,可以利用这个来定位最小值所在块,这个做法对 umap 的随机访问有强烈需求。
思维困境的破除:转置做法会给人一种错觉,“值域分块有根号个,但出现次数有 \(n^{0.75}\) 种,那空间复杂度是不是 \(n^{1.25}\) 呢”。
我们可以对其进行常规分析,由于是凸函数,最优秀的卡法显然是每块分配的修改次数相同,各 \(n\) 次,于是出现次数还是只有根号种。
但是回到原始的分析方法,一共只有 \(n\) 种数,数 - 出现次数的 pair 的个数也只有 \(n\) 种,那块编号 - 出现次数的 pair 就更少了。
将 umap 优化掉是一个经典方法,如果我们对 umap 的需求是增删遍历,而该需求在能记录迭代器且 增删无需合并(增保证不跟已有的重复,删保证不删空元素) 的的前提下是可以用链表平替的。
但倘若我们想要维护所有出现过的块编号(即做法 1),这是不满足“增不跟已有的重复”的,多个元素是同一块的时无法进行有效去重,这个做法几乎倒闭了。
其实是可以挽救的,我们对其进行惰性去重,插入时直接对链表 push_back,询问时若大小 \(\le \sqrt n\),仍然暴力遍历,若大小 \(>\sqrt n\) 我们可以利用一个大小为 \(\sqrt n\) 的桶进行去重,由于总共只有 \(n\) 个元素,惰性加入的空间不会太大。
@nalemy 的优秀优化让我们不用对那个大小为 \(n\) 的部分进行优化了,但其实第二个结构是可以打包进来的,我们可以对链表中的每个元素再维护一个链表,表示这个块中的元素都有哪些,去重时进行合并直接把两个链表接起来,是 \(\mathcal O(1)\) 的。
那转置做法需要随机访问,能不能挽救呢?尝试使用类似的方法,但值域太大,我们无法快速询问最大值的存在性,于是倒闭的有点惨。
总结
我认为去掉哈希表(即禁止映射)的数据结构体系是较为诡异的,值域小成了强而有力的工具,但类似的方法除了减小常数以外没有更大的优势,除非我们通过交互库来禁止变量的乘除及位运算,只保留加减,杜绝映射的可能性。
在目前的几乎所有用数表示地址的寻址体系中都做不到禁止映射这件事,可能需要奇怪的内存硬件结构以及寻址方式才可以实现。