P4168 [Violet] 蒲公英 题解

· · 题解

阅读提示

本题解注重思维,较为详细地叙述了提出算法及优化时空复杂度的过程。

希望读者阅读时充分地开动脑筋,不要浅尝辄止或走马观花。

题意简述

给出 n 个数的序列 a _ 1 \sim a _ n,有 m 个询问,每次给出询问区间 \left[ l , r \right],求 a _ l \sim a _ r众数最小值。(强制在线)

算法分析

区间问题常用树状数组、线段树等数据结构维护,但它们难以应用于本题。

原因是:已知左、右两段区间的众数,一般不能直接推得整个区间的众数,即使额外维护一些信息也是如此(即众数不具有“区间可加性”)。

于是考虑分块处理。

具体地,将 a _ 1 \sim a _ n 划分为 t 块,块长为 d(首尾两块可能不同)。则每个 \left[ l , r \right] 由若干连续整块,以及至多首尾两个非整块组成。

注意到,如果一个数不是连续整块的众数,则一定需要通过在非整块中出现来增加出现次数,才有可能成为 a _ l \sim a _ r 的众数。

于是得到引理:a _ l \sim a _ r 的众数只可能是 \left[ l , r \right] 对应连续整块的众数,或是在非整块中出现的数。

方法一

由上述引理,可以自然得到以下算法:

首先将数列离散化,使值域在 1 \sim n 内。

预处理 Cnt _ {i , j} \left( w \right) 为数值 w 在第 i \sim j 块出现的次数,Mode _ {i , j} 为第 i \sim j 块的众数。

对每个左端点 i,依次从 it 扫描 j,在 Cnt _ {i , j - 1} 的基础上加入第 j 块元素的计数,即得 Cnt _ {i , j}

在更新 Cnt _ {i , j} 时进行比较,顺便可得到 Mode _ {i , j}

这样,对询问区间 \left[ l , r \right],设其对应连续整块为第 i \sim j 块,则连续整块部分的众数为 Mode _ {i , j}

对于非整块中出现的数,在 Cnt _ {i , j} 的基础上,加入它们的计数。如果某个数值能够替换当前众数(指这个数值出现的次数比当前众数多,或者当出现次数相等时,数值比当前众数小),则将其替换为新的众数。

时间复杂度 O \left( n \log n + n t ^ 2 + m d \right),空间复杂度 O \left( n t ^ 2 \right)。由

O \left( n t ^ 2 + m d \right) = O \left( \dfrac {n ^ 3} {d ^ 2} + m d \right) = O \left( \dfrac {n ^ 3} {d ^ 2} + m d + m d \right) \ge O \left( \sqrt [3] {n ^ 3 m ^ 2} \right) = O \left( n m ^ {\frac 2 3} \right) .

等号成立时 \dfrac {n ^ 3} {d ^ 2} = m d,即 d = \dfrac n {\sqrt [3] m}

于是,取 d = \dfrac n {\sqrt [3] m}t = \sqrt [3] m 时,时间复杂度 O \left( n \log n + n m ^ {\frac 2 3} \right),空间复杂度 O \left( n m ^ {\frac 2 3} \right),在洛谷上已经可以 AC 了。

(代码链接在文末)

小细节

方法二

真正热爱算法竞赛的人,不应以 AC 为终极目的。

方法一的时空复杂度相对较高,虽然在洛谷能过,但在我弱校 OJ 上就够呛。说到底还是为了在我弱校 OJ 上 AC。

注意到方法一中将 Cnt _ {i , j} 赋值为 Cnt _ {i , j - 1} 的复杂度较高——单次 O \left( n \right),考虑省去这个过程。

但这意味着,当 i 不变,j 增加时,Cnt _ {i , j} 必须现用现改,无法保存了,只能保存 Mode _ {i , j}

于是对于非整块中出现的数值,要另辟蹊径查询其在 a _ l \sim a _ r 中出现的次数。

考虑对每个数值 w 开一个动态数组 v _ w \left( k \right),记录其在 a _ 1 \sim a _ n 中出现的所有位置。

这样,要查询数值 wa _ l \sim a _ r 中出现的次数,在 v _ i二分查找第一个大于 r 的位置和第一个大于等于 l 的位置,二者相减,即为答案。

剩下的操作就和方法一基本相同了。

时间复杂度 O \left( n \log n + n t + m d \log n \right),空间复杂度 O \left( n + t ^ 2 \right)。由

O \left( n t + m d \log n \right) = O \left( \dfrac {n ^ 2} d + m d \log n \right) \ge O \left( \sqrt {n ^ 2 m \log n} \right) = O \left( n \sqrt {m \log n} \right) .

等号成立时 \dfrac {n ^ 2} d = m d \log n,即 d = \dfrac n {\sqrt {m \log n}}

注意二分查找的 \log 底数取 2 较合适。于是,取 d = \dfrac n {\sqrt {m \log _ 2 n}}t = \sqrt {m \log _ 2 n} 时,时间复杂度 O \left( n \log n + n \sqrt {m \log n} \right),空间复杂度 O \left( n + m \log n \right)

小细节

方法三

以上两种方法记载于蓝书《算法竞赛进阶指南》。有没有更好的做法呢?李煜东老师告诉我们:

虽然很多问题的时间复杂度是有下界的,但从某种程度上说,算法的设计、优化是永无止境的。

在这句话的指引下,我们希望把方法二时间复杂度中的 \sqrt {\log n} 去掉,也就是省去二分查找。

于是考虑预处理 S _ i \left( w \right) 表示 w 在第 i 块中出现的次数。

\left[ l , r \right] 对应的连续整块为第 i \sim j 块,Cnt \left( w \right) 为数值 w 在非整块中出现的次数,则 wa _ l \sim a _ r 中出现的次数为

S _ j \left( w \right) - S _ {i - 1} \left( w \right) + Cnt \left( w \right).

运用这个值,在经方法二预处理得到的 Mode _ {i , j} 基础上更新众数即可。

时间复杂度 O \left( n \log n + n t + m d \right),空间复杂度 O \left( n t \right)。由

O \left( n t + m d \right) = O \left( \dfrac {n ^ 2} d + m d \right) \ge O \left( \sqrt {n ^ 2 m} \right) = O \left( n \sqrt m \right) .

等号成立时 \dfrac {n ^ 2} d = m d,即 d = \dfrac n {\sqrt m}

于是,取 d = \dfrac n {\sqrt m}t = \sqrt m 时,时间复杂度 O \left( n \log n + n \sqrt m \right),空间复杂度 O \left( n \sqrt m \right)

方法四

方法三的时间复杂度优于方法二,但空间复杂度反而劣于方法二(对数变成了根号)。我弱校 OJ 上 AC 不了。

注意到 S _ i \left( w \right) 占用空间太大,这使我们想要请回动态数组 v _ w \left( k \right)。但二分查找的问题又横在我们面前。

如果预处理每个 a _ xv _ {a _ x} \left( k \right) 中出现的位置 u _ x,那么,当查询 wa _ l \sim a _ r 中出现的次数时,如果 a _ l = w a _ r = w,则答案为 u _ r - u _ l + 1

现在问题是:a _ l = wa _ r = w 不一定满足。

但是,对每个 \left[ l , r \right] 划分出的非整块至多只有首尾两个。对首块扫描到 a _ x,一定有 \left[ x , r \right] \subseteq \left[ l , r \right]a _ l = w(不是从前到后第一次出现的元素,其贡献可以直接忽略);对尾块扫描到 a _ x,一定有 \left[ l , x \right] \subseteq \left[ l , r \right]a _ r = w(不是从后到前第一次出现的元素,其贡献可以直接忽略)。

a _ l = w a _ r = w 一定满足。

这启示我们用单调性解决问题。

具体地,预处理时,在方法二的基础上,除了保留 Mode _ {i , j},还保留连续整块众数的出现次数 Cnt _ {i , j},其他数出现次数仍不保留。

对询问区间 \left[ l , r \right],设对应的连续整块为第 i \sim j 块,当前已处理部分的众数为 mode,其出现次数为 cnt。初始 mode = Mode _ {i , j}cnt = Cnt _ {i , j}

对于首块的所有元素 a _ x

对于尾块,同理可解决该问题。

时间复杂度分析与方法三相同,但空间复杂度 O \left( n + t ^ 2 \right) = O \left( n + m \right)

实测该方法在时间上也更优,加了快读快写后竟然成了最优解(在开了 O2 并用了 vector 的情况下)。

总结

对一道好题来说,AC 只是最低要求,更重要的是让题目拓展我们的思维,激发有深度的思考,让我们切切实实感受到这道题对我们的益处。

本文提到了几个细节,也是用自己的人生经验提醒大家“细节决定成败”。

另外,感谢我弱校 OJ 对于我写这篇题解的贡献。

参考

李煜东《算法竞赛进阶指南》0x44

代码