莫队题三道(LOJ6273, CF1476G, CF700D)

LOJ6273 郁金香

题目链接

题目大意

给定长度为 \(n\) 的序列 \(a_{1\dots n}\)\(m\) 次询问,每次问一个区间 \([l, r]\) 中出现次数第 \(k\) 多的数值。如果出现次数相同,则认为较小的数出现次数较多。

数据范围:\(1\leq n, m\leq 10^5\)\(1\leq a_i\leq n\)


莫队。维护出:

  1. 每个数值的出现次数,记为 \(c\) 序列。形式化地:\(c_{i} = \sum_{j = l}^{r} [a_j = i]\)
  2. 每种“出现次数”的出现次数(即它对应了多少个数值),记为 \(c'\) 序列。形式化地:\(c'_i = \sum_{j = 1}^{n}[c_j = i]\)

考虑先求出答案的出现次数。即,求最大 \(i\),满足 \(\sum_{j = i}^{n} c'_j\geq k\)。考虑用一个数据结构来维护 \(c'\) 序列,支持单点修改,回答上述询问。因为外层使用了莫队,所以单点修改次数较多(有 \(\mathcal{O}(m\sqrt{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}(n\sqrt{n})\)

总时间复杂度 \(\mathcal{O}(n\sqrt{n})\),空间复杂度 \(\mathcal{O}(n\sqrt{n})\)\(n, m\) 同阶)。


前面说的“分块”方法,你可以概括性地理解为,它是一种数据结构,支持:

  1. 单点修改。做法是:修改时更新该位置所在的块的信息(块内和)。时间复杂度 \(\mathcal{O}(1)\)
  2. 查询整个序列中,第一个前缀和 \(\geq k\) 的位置(或最后一个后缀和 \(\geq k\) 的位置)。做法是:逐个块扫描,完整块的信息已经维护好,这样可以快速定位到答案所在的块。时间复杂度 \(\mathcal{O}(\sqrt{n})\)

上述两条,用平衡树来理解,就是插入/删除元素,查询第 \(k\) 大/第 \(k\) 小的元素。

特别地,2 操作中,如果只需要查询答案所在的块,而不需要知道具体位置,那么空间复杂度可以 \(\mathcal{O}(\sqrt{n})\)。本题里,对每种出现次数 \(i\) 维护的 \(f_{i}\) 就是这样一个空间复杂度 \(\mathcal{O}(\sqrt{n})\) 的数据结构,我们用它来代替了平衡树。

参考代码-在LOJ查看


总结:因为有关数值的出现次数,容易想到用莫队来维护。然后要使用“分块”的数据结构,它常与莫队等其他根号算法结合,用于根号平衡。

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 i\in[1, k], j\in[1, k]: |c_{x_i} - c_{x_j}|\leq k\)。如果区间里出现的数不到 \(k\) 个,输出 \(-1\)
  • \(2\ p\ x\)。表示将位置 \(p\) 上的数修改为 \(x\)。即 \(a_p\gets x\)

数据范围:\(1\leq n,m, a_i\leq 10^5\)


用带修改的莫队(三维莫队)。维护出:

  1. 每个数值的出现次数,记为 \(c\) 序列。形式化地:\(c_{i} = \sum_{j = l}^{r} [a_j = i]\)
  2. 每种“出现次数”的出现次数(即它对应了多少个数值),记为 \(c'\) 序列。形式化地:\(c'_i = \sum_{j = 1}^{n}[c_j = i]\)

时间复杂度 \(\mathcal{O}(n^{\frac{3}{5}})\)。具体分析可以参考三维莫队的教程。

那么问题转化为:求一对 \(i, j\)\(1\leq i\leq j\leq n\))满足 \(\sum_{t = i}^{j} c'_t\geq k\),要求最小化 \(j - i\) 的值。

这个问题单独做是很困难的。于是考虑 \(c'\) 序列的实际意义:\(c'_i\) 表示(当前区间 \([l, r]\) 里)出现次数为 \(i\) 的数值的数量。所以:\(\sum_{i = 1}^{n}c'_i\cdot i = r - l + 1\leq n\)。这意味着,\(c'_i > 0\) 的不同的 \(i\),数量是 \(\mathcal{O}(\sqrt{n})\) 的。

维护出所有 \(c'_i > 0\)\(i\) 的集合(不需要保证集合有序)。也就是仅支持插入和删除,这可以 \(\mathcal{O}(1)\) 实现。

每次询问时,将 \(c'_i > 0\) 的这些 \(i\),从小到大排序(用 \(\texttt{std::sort}\) 暴力排,反正它们数量只有 \(\mathcal{O}(\sqrt{n})\))。然后 two pointers 扫一遍,即可求出答案。

时间复杂度 \(\mathcal{O}(n^{\frac{3}{5}} + n\sqrt{n}\log n)\)\(n, m\) 同阶)。

参考代码-在CF查看


总结:难点在于莫队之后,要结合实际意义,利用其特殊的性质,从而解决看似“不可做”的问题。

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}\) 编码的最小长度。

数据范围:\(1\leq n, q, a_i\leq 10^5\)


一种编码方案,可以看做一棵二叉树。从根出发,向左走一步表示一个 \(0\),向右走一步表示一个 \(1\)。每个字符,都是二叉树上的一个叶子。设第 \(i\) 种字符的出现次数为 \(c_i\),在二叉树上的深度为 \(d_i\),则该编码方案的长度就是:\(\sum_{i} c_i\cdot d_i\)。我们要构造出一棵二叉树,来最小化这个值。

这是一个经典的问题。可以用贪心法求解。

设有 \(k\) 种字符。初始时,只有 \(k\) 个节点,它们之间还没有连边。可以看做是 \(k\) 棵树,每个节点都是根。每个点的点权是 \(c_i\)。每次选择两个点权最小的根节点,将它们合并。即:新建一个节点,作为它们的父亲,新节点的点权是原来两个根节点的点权之和。同时因为两个节点高度都增加了 \(1\),所以答案要加上这两个节点的点权和。

\[\begin{array}{l} \textbf{def: } \mathrm{calc}(c_1, c_2, \dots ,c_k) \\ \qquad S \leftarrow \{c_i\}\\ \qquad \mathrm{res}\leftarrow 0\\ \qquad \textbf{while } (|S| > 1) \\ \qquad \qquad a \leftarrow \min\{S\} \\ \qquad \qquad S \leftarrow S \setminus a\\ \qquad \qquad b \leftarrow \min\{S\} \\ \qquad \qquad S \leftarrow S \setminus b\\ \qquad \qquad \mathrm{res}\leftarrow \mathrm{res} + a + b\\ \qquad \qquad S \leftarrow S \cup \{a + b\}\\ \qquad \textbf{endwhile.} \\ \textbf{enddef.} \end{array} \]

可以用 \(\texttt{std::priority_queue}\)(优先队列)实现上述过程。时间复杂度 \(\mathcal{O}(k \log k)\)

本题里,我们要处理 \(q\) 次询问。朴素做法的时间复杂度是 \(\mathcal{O}(qn\log n)\),无法通过。

可以用莫队算法来维护每个数值的出现次数,这部分的时间复杂度是 \(\mathcal{O}(q\sqrt{n})\)

然后如何计算答案呢?因为没有直观的做法,所以考虑根号分治:把两个暴力拼起来!设置一个阈值 \(B\)

  • 对于区间里,出现次数 \(\geq B\) 的数,这样的数最多只有 \(\frac{n}{B}\) 个,可以直接用优先队列实现上述的贪心做法,时间复杂度 \(\mathcal{O}(\frac{n}{B}\log \frac{n}{B})\)
  • 出现次数 \(< B\) 的数,它们的数量可能很多。所以我们不枚举这些数。而是直接枚举它们的出现次数,即 \(1\dots B - 1\)。可以提前用一个数组存好,每种出现次数对应了多少个数值。从 \(1\)\(B - 1\) 扫描所有出现次数,模拟上述贪心做法的过程:设当前扫描到的出现次数为 \(i\),对应的数值有 \(t_i\) 个,那么将它们两两合并为 \(2i\),即:\(t_{2i}\leftarrow t_{2i} + \frac{t_i}{2}\)。特别地,如果 \(2i\geq B\),则直接暴力将这 \(\frac{t_i}{2}\) 个数加入优先队列,和后面(出现次数 \(\geq B\))的数放在一起贪心。另外,\(t_i\) 是奇数时,需要注意一些细节。

下面分析第二种情况的时间复杂度,首先扫描一遍肯定是 \(\mathcal{O}(B)\) 的。重点在于 \(2i\geq B\) 时,暴力将 \(\frac{t_i}{2}\) 个数加入优先队列,总共会加几次。我们每次操作会令 \(t_{2i}\leftarrow t_{2i} + \frac{t_i}{2}\)。发现不管操作多少次,\(\sum i\cdot t_i\) 始终是不变的。因为初始时 \(\sum i\cdot t_i\leq n\),所以最终的 \(\sum_{i\geq B} i\cdot t_i\) 还是 \(\leq n\) 的。我们加入优先队列的元素数量,就是此时的 \(\sum_{i\geq B} t_i\),它是 \(\mathcal{O}(\frac{n}{B})\) 级别的。

于是我们能够在 \(\mathcal{O}(B + \frac{n}{B}\log \frac{n}{B})\) 的时间复杂度内回答单次询问。取 \(B = \sqrt{n\log n}\)。总时间复杂度 \(\mathcal{O}(q\sqrt{n \log n})\)。(大概)。

参考代码-在CF查看

总结

有关“序列、区间里、数值的出现次数”的问题,一般都要想到用莫队来维护。

莫队后,往往就是要维护一个序列,支持单点修改,然后回答各种各样的询问。因为外层套了莫队,所以需要 \(\mathcal{O}(1)\) 的单点修改,而询问则可以较慢。一般的数据结构(如线段树、平衡树),修改和查询都是 \(\mathcal{O}(\log n)\) 的,往往难以胜任。此时有如下的一些方法:

  • 用分块代替一般的数据结构。
  • 根据实际意义,分析要维护的数组的特殊性质。

(欢迎读者继续补充。)

posted @ 2021-02-04 10:55  duyiblue  阅读(331)  评论(0编辑  收藏  举报