LOJ6273 郁金香
题目大意:
给定长度为 (n) 的序列 (a_{1dots n})。(m) 次询问,每次问一个区间 ([l, r]) 中出现次数第 (k) 多的数值。如果出现次数相同,则认为较小的数出现次数较多。
数据范围:(1leq n, mleq 10^5),(1leq a_ileq n)。
莫队。维护出:
- 每个数值的出现次数,记为 (c) 序列。形式化地:(c_{i} = sum_{j = l}^{r} [a_j = i])。
- 每种“出现次数”的出现次数(即它对应了多少个数值),记为 (c') 序列。形式化地:(c'_i = sum_{j = 1}^{n}[c_j = i])。
考虑先求出答案的出现次数。即,求最大 (i),满足 (sum_{j = i}^{n} c'_jgeq k)。考虑用一个数据结构来维护 (c') 序列,支持单点修改,回答上述询问。因为外层使用了莫队,所以单点修改次数较多(有 (mathcal{O}(msqrt{n})) 次),而询问只有 (mathcal{O}(m)) 次,所以采用“根号平衡”的思想,用分块来维护 (c') 序列。这样修改是 (mathcal{O}(1)) 的,询问是 (mathcal{O}(sqrt{n})) 的。具体方法后面会说。
现在已经知道了答案的出现次数 (i),但还不知道具体的数值。设 (k' = k - sum_{j = i + 1}^{n}c'_j)。问题转化为,求出现次数为 (i) 的数里,第 (k') 小的。朴素的想法是对每个出现次数 (i),维护一棵平衡树,支持:插入一个数,删除一个数,查询第 (k) 大值。这样修改(插入/删除)和查询都 (mathcal{O}(log n)) 的。但(again)我们的修改次数很多,而查询次数较少。所以仍然考虑根号平衡。在值域上分块。然后维护数值 (f_{i, j}) 表示出现次数为 (i) 的数值里,数值大小位于第 (j) 块的有多少个。这样相当于插入和删除都是 (mathcal{O}(1)) 的,询问是 (mathcal{O}(sqrt{n})) 的。总的空间复杂度 (mathcal{O}(nsqrt{n}))。
总时间复杂度 (mathcal{O}(nsqrt{n})),空间复杂度 (mathcal{O}(nsqrt{n}))((n, m) 同阶)。
前面说的“分块”方法,你可以概括性地理解为,它是一种数据结构,支持:
- 单点修改。做法是:修改时更新该位置所在的块的信息(块内和)。时间复杂度 (mathcal{O}(1))。
- 查询整个序列中,第一个前缀和 (geq k) 的位置(或最后一个后缀和 (geq k) 的位置)。做法是:逐个块扫描,完整块的信息已经维护好,这样可以快速定位到答案所在的块。时间复杂度 (mathcal{O}(sqrt{n}))。
上述两条,用平衡树来理解,就是插入/删除元素,查询第 (k) 大/第 (k) 小的元素。
特别地,2 操作中,如果只需要查询答案所在的块,而不需要知道具体位置,那么空间复杂度可以 (mathcal{O}(sqrt{n}))。本题里,对每种出现次数 (i) 维护的 (f_{i}) 就是这样一个空间复杂度 (mathcal{O}(sqrt{n})) 的数据结构,我们用它来代替了平衡树。
总结:因为有关数值的出现次数,容易想到用莫队来维护。然后要使用“分块”的数据结构,它常与莫队等其他根号算法结合,用于根号平衡。
CF1476G Minimum Difference
题目大意:
给出一个长度为 (n) 的序列 (a)。(m) 次操作,是如下两种之一:
- (1 l r k)。设 (c_i) 为区间 ([l, r]) 里数值 (i) 的出现次数。该操作表示查询一个最小的 (d),使得存在 (k) 个在区间里出现过的数 (x_1, x_2, dots ,x_k),满足 (forall iin[1, k], jin[1, k]: |c_{x_i} - c_{x_j}|leq k)。如果区间里出现的数不到 (k) 个,输出 (-1)。
- (2 p x)。表示将位置 (p) 上的数修改为 (x)。即 (a_pgets x)。
数据范围:(1leq n,m, a_ileq 10^5)。
用带修改的莫队(三维莫队)。维护出:
- 每个数值的出现次数,记为 (c) 序列。形式化地:(c_{i} = sum_{j = l}^{r} [a_j = i])。
- 每种“出现次数”的出现次数(即它对应了多少个数值),记为 (c') 序列。形式化地:(c'_i = sum_{j = 1}^{n}[c_j = i])。
时间复杂度 (mathcal{O}(n^{frac{3}{5}}))。具体分析可以参考三维莫队的教程。
那么问题转化为:求一对 (i, j)((1leq ileq jleq n))满足 (sum_{t = i}^{j} c'_tgeq k),要求最小化 (j - i) 的值。
这个问题单独做是很困难的。于是考虑 (c') 序列的实际意义:(c'_i) 表示(当前区间 ([l, r]) 里)出现次数为 (i) 的数值的数量。所以:(sum_{i = 1}^{n}c'_icdot i = r - l + 1leq n)。这意味着,(c'_i > 0) 的不同的 (i),数量是 (mathcal{O}(sqrt{n})) 的。
维护出所有 (c'_i > 0) 的 (i) 的集合(不需要保证集合有序)。也就是仅支持插入和删除,这可以 (mathcal{O}(1)) 实现。
每次询问时,将 (c'_i > 0) 的这些 (i),从小到大排序(用 ( exttt{std::sort}) 暴力排,反正它们数量只有 (mathcal{O}(sqrt{n})))。然后 two pointers 扫一遍,即可求出答案。
时间复杂度 (mathcal{O}(n^{frac{3}{5}} + nsqrt{n}log n))((n, m) 同阶)。
总结:难点在于莫队之后,要结合实际意义,利用其特殊的性质,从而解决看似“不可做”的问题。
CF700D Huffman Coding on Segment
题目大意:
考虑对一个字符串(本题里用数字序列表示)进行二进制编码。使得:
- 字符串里出现过的每个字符,都对应一个二进制编码。
- 不存在两个字符 (i, j),使得 (i) 的编码是 (j) 的编码的前缀。
一种编码方案的“长度”,是指把字符串里每个字符对应的编码顺次连接起来,得到的二进制串的长度。
给定一个长度为 (n) 的序列 (a)。(q) 次询问。每次询问给出区间 (l_i, r_i),求给字符串 (a_{l_i}, a_{l_i + 1},dots,a_{r_i}) 编码的最小长度。
数据范围:(1leq n, q, a_ileq 10^5)。
一种编码方案,可以看做一棵二叉树。从根出发,向左走一步表示一个 (0),向右走一步表示一个 (1)。每个字符,都是二叉树上的一个叶子。设第 (i) 种字符的出现次数为 (c_i),在二叉树上的深度为 (d_i),则该编码方案的长度就是:(sum_{i} c_icdot d_i)。我们要构造出一棵二叉树,来最小化这个值。
这是一个经典的问题。可以用贪心法求解。
设有 (k) 种字符。初始时,只有 (k) 个节点,它们之间还没有连边。可以看做是 (k) 棵树,每个节点都是根。每个点的点权是 (c_i)。每次选择两个点权最小的根节点,将它们合并。即:新建一个节点,作为它们的父亲,新节点的点权是原来两个根节点的点权之和。同时因为两个节点高度都增加了 (1),所以答案要加上这两个节点的点权和。
可以用 ( exttt{std::priority_queue})(优先队列)实现上述过程。时间复杂度 (mathcal{O}(k log k))。
本题里,我们要处理 (q) 次询问。朴素做法的时间复杂度是 (mathcal{O}(qnlog n)),无法通过。
可以用莫队算法来维护每个数值的出现次数,这部分的时间复杂度是 (mathcal{O}(qsqrt{n}))。
然后如何计算答案呢?因为没有直观的做法,所以考虑根号分治:把两个暴力拼起来!设置一个阈值 (B)。
- 对于区间里,出现次数 (geq B) 的数,这样的数最多只有 (frac{n}{B}) 个,可以直接用优先队列实现上述的贪心做法,时间复杂度 (mathcal{O}(frac{n}{B}log frac{n}{B}))。
- 出现次数 (< B) 的数,它们的数量可能很多。所以我们不枚举这些数。而是直接枚举它们的出现次数,即 (1dots B - 1)。可以提前用一个数组存好,每种出现次数对应了多少个数值。从 (1) 到 (B - 1) 扫描所有出现次数,模拟上述贪心做法的过程:设当前扫描到的出现次数为 (i),对应的数值有 (t_i) 个,那么将它们两两合并为 (2i),即:(t_{2i}leftarrow t_{2i} + frac{t_i}{2})。特别地,如果 (2igeq B),则直接暴力将这 (frac{t_i}{2}) 个数加入优先队列,和后面(出现次数 (geq B))的数放在一起贪心。另外,(t_i) 是奇数时,需要注意一些细节。
下面分析第二种情况的时间复杂度,首先扫描一遍肯定是 (mathcal{O}(B)) 的。重点在于 (2igeq B) 时,暴力将 (frac{t_i}{2}) 个数加入优先队列,总共会加几次。我们每次操作会令 (t_{2i}leftarrow t_{2i} + frac{t_i}{2})。发现不管操作多少次,(sum icdot t_i) 始终是不变的。因为初始时 (sum icdot t_ileq n),所以最终的 (sum_{igeq B} icdot t_i) 还是 (leq n) 的。我们加入优先队列的元素数量,就是此时的 (sum_{igeq B} t_i),它是 (mathcal{O}(frac{n}{B})) 级别的。
于是我们能够在 (mathcal{O}(B + frac{n}{B}log frac{n}{B})) 的时间复杂度内回答单次询问。取 (B = sqrt{nlog n})。总时间复杂度 (mathcal{O}(qsqrt{n log n}))。(大概)。
总结
有关“序列、区间里、数值的出现次数”的问题,一般都要想到用莫队来维护。
莫队后,往往就是要维护一个序列,支持单点修改,然后回答各种各样的询问。因为外层套了莫队,所以需要 (mathcal{O}(1)) 的单点修改,而询问则可以较慢。一般的数据结构(如线段树、平衡树),修改和查询都是 (mathcal{O}(log n)) 的,往往难以胜任。此时有如下的一些方法:
- 用分块代替一般的数据结构。
- 根据实际意义,分析要维护的数组的特殊性质。
(欢迎读者继续补充。)