P3246 [HNOI2016] 序列 解题报告
P3246 [HNOI2016] 序列 解题报告
一、 题目解读
首先,我们需要理解题目的要求。给定一个长度为 n
的数列 a
,以及 q
次询问。每次询问会给出一个区间 [l, r]
。我们需要计算这个区间 a[l:r]
的 所有连续子区间 的 最小值之和。
举个例子,如果序列是 a = {5, 2, 4}
,询问区间是 [1, 3]
。
它的所有连续子区间有:
[1, 1]
:{5}
,最小值为 5[2, 2]
:{2}
,最小值为 2[3, 3]
:{4}
,最小值为 4[1, 2]
:{5, 2}
,最小值为 2[2, 3]
:{2, 4}
,最小值为 2[1, 3]
:{5, 2, 4}
,最小值为 2
所以,答案就是 5 + 2 + 4 + 2 + 2 + 2 = 17
。
由于 n
和 q
的规模都达到了 100000,对于每次询问都暴力枚举所有子区间(时间复杂度为 O(n²))是不可接受的,总时间复杂度会达到 O(q * n²),一定会超时。因此,我们需要更高效的算法。
二、 核心思路:分治与预处理
这道题最优秀、最核心的解法是一种基于 分治思想 的在线算法。在线算法意味着我们对每个询问都可以独立、快速地计算出答案,而不需要像离线算法那样把所有询问先读入再一并处理。
核心思想:对于一个询问区间 [l, r]
,我们可以找到这个区间里的最小值 a[p]
,其中 p
是最小值对应的下标。现在,所有 [l, r]
的子区间可以被分成三类:
- 完全在
p
左侧的子区间:即子区间的范围是[i, j]
,满足l <= i <= j < p
。 - 完全在
p
右侧的子区间:即子区间的范围是[i, j]
,满足p < i <= j <= r
。 - 跨越
p
的子区间:即子区间的范围是[i, j]
,满足l <= i <= p <= j <= r
。
1. 计算跨越 p
的子区间的贡献
这是最简单的一部分。对于任何一个跨越了 p
的子区间 [i, j]
,因为 a[p]
是整个 [l, r]
区间的最小值,所以它也一定是 [i, j]
这个子区间的最小值。
我们只需要数一数有多少个这样的子区间即可。
- 左端点
i
可以选在[l, p]
,有p - l + 1
种选择。 - 右端点
j
可以选在[p, r]
,有r - p + 1
种选择。
所以,跨越 p
的子区间一共有 (p - l + 1) * (r - p + 1)
个,它们对总和的贡献就是 a[p] * (p - l + 1) * (r - p + 1)
。
2. 计算左右两侧子区间的贡献
现在,问题转化为了两个子问题:计算 [l, p-1]
所有子区间的最小值之和,以及 [p+1, r]
所有子区间的最小值之和。
直接递归解决这两个子问题会导致大量的重复计算,效率依然不高。这里的关键就是 “预处理”。我们可以预先计算出一些辅助信息,使得这两个子问题的答案能够被快速查询。
三、 关键的预处理步骤
为了能够快速得到任意形如 Ans(x, y)
(即区间 [x,y]
的答案)的值,我们需要进行一系列的预处理。
1. 单调栈:计算 pre[i]
和 suf[i]
pre[i]
: 在i
左边第一个比a[i]
小的数的下标。suf[i]
: 在i
右边第一个比a[i]
小的数的下标。
这两个数组可以使用单调栈在 O(n) 的时间内线性预处理出来。它们对于计算一个数作为最小值的“管辖范围”至关重要。
2. 动态规划:计算 fr[i]
和 fl[i]
fr[i]
: 以i
为右端点 的所有子区间[k, i]
(1 <= k <= i) 的最小值之和。fl[i]
: 以i
为左端点 的所有子区间[i, k]
(i <= k <= n) 的最小值之和。
我们以 fr[i]
为例推导其递推式:
考虑以 i
为右端点的所有子区间 [k, i]
。
- 当
k
在(pre[i], i]
这个范围内时,a[i]
是a[k...i]
中的最小值。这部分区间的贡献是a[i] * (i - pre[i])
。 - 当
k
在[1, pre[i]]
这个范围内时,a[k...i]
的最小值等于a[k...pre[i]]
的最小值(因为a[pre[i]]
比a[i]
右侧到i
为止的所有数都小)。这部分的贡献之和恰好就是fr[pre[i]]
的定义。
所以我们得到递推式:fr[i] = fr[pre[i]] + a[i] * (i - pre[i])
。
同理可得:fl[i] = fl[suf[i]] + a[i] * (suf[i] - i)
。
这两个数组也可以在 O(n) 时间内计算出来。
3. 前缀和/后缀和:计算 gr[i]
和 gl[i]
gr[i]
:fr
数组的前缀和,gr[i] = fr[1] + fr[2] + ... + fr[i]
。gl[i]
:fl
数组的后缀和,gl[i] = fl[i] + fl[i+1] + ... + fl[n]
。
这两个数组显然也可以在 O(n) 内计算。
四、 整合与最终公式
经过上述预处理,我们现在拥有了强大的工具来解决子问题。可以证明(推导过程较为复杂,但结论很优美),我们之前提到的两个子问题的答案可以通过预处理出的数组组合得到:
[p+1, r]
所有子区间的最小值之和 =gr[r] - gr[p] - fr[p] * (r - p)
[l, p-1]
所有子区间的最小值之和 =gl[l] - gl[p] - fl[p] * (p - l)
最终,对于一个询问 [l, r]
的完整答案公式为:
Ans(l, r) = a[p]*(p-l+1)*(r-p+1) + (gr[r]-gr[p]-fr[p]*(r-p)) + (gl[l]-gl[p]-fl[p]*(p-l))
其中 p
是区间 [l,r]
最小值的下标。
五、 算法实现与复杂度
-
预处理 (O(n log n) 或 O(n))
- 用单调栈计算
pre
和suf
数组,O(n)。 - 递推计算
fr
,fl
,gr
,gl
数组,O(n)。 - 为了在查询时快速找到区间
[l, r]
的最小值下标p
,我们需要一个 RMQ (Range Minimum Query) 数据结构。使用 Sparse Table (ST表) 实现 RMQ,预处理复杂度为 O(n log n),查询为 O(1)。
- 用单调栈计算
-
查询 (O(1) per query)
- 对于每个询问
[l, r]
,使用 ST 表找到p
。 - 代入上面的最终公式,进行 O(1) 的计算。
- 对于每个询问
总时间复杂度为 O(n log n + q),足以通过本题。
六、 另一种思路:莫队算法
题解中还提到了莫队算法。这是一种强大的离线算法,专门处理不带修改的区间查询问题。
- 核心思想: 将所有查询离线下来,通过巧妙的排序,使得处理相邻两个查询时,区间的左右端点
l, r
的移动总距离最小。 - 本题难点: 如何在
O(1)
或O(log n)
的时间内,计算出区间[l, r]
扩展或缩减一个单位时,答案的变化量。例如,从[l, r]
变为[l, r+1]
,需要增加所有以r+1
为右端点、以[l, r+1]
中某个位置为左端点的子区间的最小值之和。 - 解决方案: 这个“增量”的计算同样可以利用我们之前预处理的
fr
,fl
数组以及 RMQ 来实现。 - 复杂度: 莫队算法的时间复杂度通常为
O(n√n)
,如果增量计算需要 RMQ,则为O(n√n * log n)
。对于本题,这个复杂度也可以接受,但比在线的分治算法要慢。
总结
本题是一道优秀的数据结构与算法思维结合的题目。最高效的解法是基于 分治 思想,将一个大区间的复杂问题,根据其最小值划分为一个简单的核心部分和两个结构相同的子问题。然后通过 动态规划 和 前缀和 等预处理手段,将子问题的求解优化到 O(1),从而实现了对每个查询的快速响应。
一些思考
我们来用更通俗的方式,一步一步地拆解和推导这两个公式。我会尽量避免复杂的数学符号,而用“算账”的思路来解释。
核心思想是 “先多算,再减掉多算的部分”。
1. 解释第一个公式:右半部分 [p+1, r]
的答案
公式: [p+1, r]
所有子区间的最小值之和 = gr[r] - gr[p] - fr[p] * (r - p)
我们的目标是:求出所有起点和终点都在 [p+1, r]
这个范围内的子区间的最小值之和。
第一步:来个“毛估估”,先算一个大概的、多算了很多的值
我们手头有什么工具?有 fr
和 gr
数组。
fr[i]
:以i
为右端点的所有子区间([k, i]
,其中1 <= k <= i
)的最小值之和。gr[i]
:fr
的前缀和,即fr[1] + fr[2] + ... + fr[i]
。
一个很自然的想法是,我们要求的答案,和所有以 p+1, p+2, ..., r
为右端点的子区间相关。那我们干脆把 fr[p+1]
, fr[p+2]
, ..., fr[r]
全加起来怎么样?
fr[p+1] + fr[p+2] + ... + fr[r]
这个值正好可以通过前缀和 gr
快速算出,它就等于 gr[r] - gr[p]
。
好,现在我们得到了 gr[r] - gr[p]
,但它是不是我们想要的最终答案呢?显然不是。
第二步:分析我们“多算”了什么
让我们看看 fr[i]
的定义。fr[i]
是以 i
为右端点,但左端点 k
是从 1
开始一直到 i
的。
当我们计算 gr[r] - gr[p]
时,我们加起来的这些 fr[i]
(比如 fr[p+3]
),它里面包含了像 [1, p+3]
, [2, p+3]
, ..., [p, p+3]
这样我们不想要的子区间。我们只想要那些左端点也在 [p+1, r]
范围内的子区间。
也就是说,对于每一个 i
(从 p+1
到 r
),我们在加 fr[i]
的时候,都错误地计入了左端点在 [1, p]
范围内的那些子区间。我们需要把这部分“错误多算”的账给减掉。
第三步:计算“多算”的部分究竟是多少
现在问题来了,对于一个固定的 i
(p < i <= r
),它多算的这部分 (min[1,i] + min[2,i] + ... + min[p,i])
等于多少?
这里就要用到本题最关键的一个性质了:p
是区间 [l, r]
的最小值点。
这意味着,对于任何一个在 p
左边的 k
(k <= p
) 和任何一个在 p
右边的 i
(i > p
),当它们组成区间 [k, i]
时,这个区间的最小值是谁?
因为 a[p]
比 a[p+1]
到 a[r]
之间的所有数都小或相等,所以 a[k...i]
的最小值,一定和 a[k...p]
的最小值是一样的。
即: min(a[k...i]) = min(a[k...p])
(其中 k <= p < i
)。
这下就好办了!对于 fr[i]
,我们多算的部分是:
sum_{k=1 to p} min(a[k...i])
根据上面的性质,它就等于:
sum_{k=1 to p} min(a[k...p])
而 sum_{k=1 to p} min(a[k...p])
这个式子,恰好就是 fr[p]
的定义!
结论:对于任何一个 i > p
,我们计算 fr[i]
时,都比我们实际需要的多算了 fr[p]
这么多的值。
第四步:汇总,得出最终公式
我们总共考虑了 p+1, p+2, ..., r
这 r-p
个右端点。
对于每一个右端点,我们都多算了 fr[p]
。
所以,总共多算的值就是 fr[p] * (r - p)
。
好了,现在可以结账了:
- 我们一开始的毛估估是
gr[r] - gr[p]
。 - 我们发现总共多算了
fr[p] * (r - p)
。 - 所以,正确的答案就是
(gr[r] - gr[p]) - (fr[p] * (r - p))
。
公式推导完毕!
2. 解释第二个公式:左半部分 [l, p-1]
的答案
公式: [l, p-1]
所有子区间的最小值之和 = gl[l] - gl[p] - fl[p] * (p - l)
这个公式的推导是完全对称的,我们再走一遍流程:
第一步:毛估估
我们要求所有起点和终点都在 [l, p-1]
内的子区间。这次我们用 fl
和 gl
数组。
fl[i]
:以i
为左端点的所有子区间([i, k]
,其中i <= k <= n
)的最小值之和。gl[i]
:fl
的后缀和,即fl[i] + fl[i+1] + ... + fl[n]
。
我们把 fl[l]
, fl[l+1]
, ..., fl[p-1]
加起来,得到 gl[l] - gl[p]
。
第二步:分析多算了什么
fl[i]
的定义里,右端点 k
可是一直延伸到 n
的。
当我们把 fl[l]
到 fl[p-1]
相加时,对于每一个 fl[i]
(l <= i < p
),我们都包含了像 [i, p]
, [i, p+1]
, ..., [i, n]
这样我们不想要的、终点越过了 p-1
的子区间。
第三步:计算多算的部分
对于一个固定的 i
(l <= i < p
),它多算的这部分 (min[i,p] + min[i,p+1] + ... + min[i,n])
等于多少?
再次利用核心性质:p
是区间 [l, r]
的最小值点。
对于任何 l <= i < p
和 k >= p
,区间 [i, k]
横跨了 p
。因为 a[p]
比它左边 [l, p-1]
范围内的任何数都小或相等,所以:
min(a[i...k]) = min(a[p...k])
(其中 i < p <= k
)
多算的部分是:sum_{k=p to n} min(a[i...k])
根据上面的性质,它等于:
sum_{k=p to n} min(a[p...k])
而 sum_{k=p to n} min(a[p...k])
这个式子,恰好就是 fl[p]
的定义!
结论:对于任何一个 i < p
,我们计算 fl[i]
时,都比我们实际需要的多算了 fl[p]
这么多的值。
第四步:汇总,得出公式
我们总共考虑了 l, l+1, ..., p-1
这 (p-1) - l + 1 = p - l
个左端点。
每一个都多算了 fl[p]
。
总共多算的就是 fl[p] * (p - l)
。
最终结账:
- 毛估估:
gl[l] - gl[p]
- 多算的部分:
fl[p] * (p - l)
- 正确答案:
(gl[l] - gl[p]) - (fl[p] * (p - l))
总结
这两个公式的本质都是:
- 利用前缀和/后缀和 (
gr
,gl
) 对一个范围内的fr
或fl
值进行求和,得出一个过大的估算值。 - 分析这个估算值“过大”在哪里:它包含了许多越界的子区间(即起点或终点在目标范围之外)。
- 利用区间最小值
a[p]
的性质,证明所有这些越界的、需要被减掉的贡献,对于范围内的每一个端点来说,其总和都恰好等于一个固定的值(fr[p]
或fl[p]
)。 - 用这个固定的“误差值”乘以范围的长度,就是总的误差,把它减掉,就得到了精确的答案。
希望这个分步“算账”式的解释能帮助你理解这两个公式的巧妙之处!