梅花岭畔月 留待多情者

发布时间 2023-08-30 15:18:48作者: Reliauk

一些背景和定义

在数据结构题中,有一些问题的询问是关于历史版本的,除了大部分考察可持久化数据结构的问题,剩下一类特殊的问题被称为“历史最值问题”。

历史最值问题的询问通常与“历史最大值”、“历史最小值”、“历史版本和”有关,而此时相应地包含名为“扩展版本”的修改。

值得一提的是,相当一部分题目是在每一次对原序列的修改后都执行一次“扩展版本”,下文中若无特殊提及“扩展版本”,就默认是此类情况;否则会说“动态扩展版本”。

它们的形式化定义是什么?

让我们定义辅助数组 \(B\),而令题目中直接修改的数组为 \(A\)

对于历史最大值、历史最小值,一开始 \(B = A\);对于历史版本和问题,一开始 \(B=0\)

每次“扩展版本”时,我们令 \(B_i \leftarrow \max(B_i, A_i) \ \text{or} \ \min(B_i, A_i) \ \text{or} \ B_i + A_i\),对应的分别是“历史最大值”、“历史最小值”和“历史版本和”。

此时我们的询问就是基于 \(B\) 数组的。


关于历史最值问题,可以通过处理方法的不同,从易到难划分为四种情况,接下来会按顺序介绍。

懒标记可以处理的问题

在此类问题中,我们会使用带懒标记的线段树解决。

设计节点信息和标记

首先节点信息和普通的线段树节点信息设计是一样的——不断把合并要用到的辅助信息加入即可。接下来我们讨论的都是标记的设计。

我们可以假设在线段树的每个节点上有一个隐式的标记队列,此时标记是不合并的,只会在修改时加入对应节点的队列,或下传标记时把当前节点的队列分别合并到两个子结点的队列后面,再清空当前节点的队列。

直接应用这种做法虽然能从正确性上解决问题,但时间复杂度是不能接受的。

因此,我们寻求这个标记队列的一个概括,这也就是我们最终的懒标记,它需要满足以下要求:

  • 在修改时,新的标记可以快速和已有的概括合并。
  • 在下传时,概括能够快速合并,并且概括的信息形式是封闭的(封闭是指不需要其他的辅助信息就可以进行合并)。
  • 概括没有丢失更新节点信息需要的关键值。

大致上,我们可以通过对单个标记对节点信息的贡献进行复合,并提取最终式的特征值。最后再像维护节点信息那样不断加入合并用到的辅助信息。

来几道例题感知一下。

试试看

线段树基础题 from cmd's blog

区间加,查询区间历史最大值。

解答

节点信息:最大值 \(x\),历史最大值 \(m\)

标记队列里只有加法标记,考虑一个 \(+k\) 对最终答案的影响:

  • \(x \leftarrow x + k\)\(m \leftarrow \max(m, x)\)

那么我们把所有的单标记贡献求和(因为贡献的形式是加法),有:

\[\begin{aligned} x &= x + \sum_{i=1}^q k_i\\ m &= m + \max_{i=0}^q(\sum_{j=1}^i k_j)\\ &= m + \max_{i=0}^q pre_j \end{aligned} \]

其中 \(pre\)\(k\) 的前缀和。

令概括为 \(\max_{i=0}^q pre_i\)\(\sum_{i=1}^q k_i\) 即可,容易发现此时形式封闭,读者不难自行验证。


铃原露露(改)

区间加,动态扩展版本,查询区间历史最小值个数。

解答

节点信息:最小值 \(mn\),最小值个数 \(cnt\),历史最小值 \(hmn\),历史最小值个数 \(ans\)

标记队列里有加法标记和扩展版本的标记,我们不好分析多种标记的情况如何概括,因此考虑转化

容易发现在两次扩展版本标记之间的加法标记可以被缩成一次(简单把加法标记的值加起来就好了),因为中间没有扩展版本标记,就不会中途造成贡献

我们记录 \(k_i\) 表示第 \(i - 1\) 个扩展版本标记和第 \(i\) 个扩展版本标记之间的加法标记缩成了什么值。

那么对于每个 \(k_i\) 仍有:

  • \(mn \leftarrow mn + k_i\)\(hmn \leftarrow \min(hmn, mn)\)
  • \(ans \leftarrow ans + [hmn = mn]cnt\)

对于所有的 \(k_i\) 复合贡献,可以得到:

\[\begin{aligned} mn &= mn + \sum k_i\\ cnt &= cnt \\ hmn &= mn + \min_{i=1}^q pre_i\\ ans &= ans + \sum[pre_j = \min_{i=1}^q pre_i] cnt \end{aligned} \]

因此我们只需要在概括中记录 \(\sum_i k_i\)\(\min_i pre_i\)\(\sum_j[pre_j = \min_i pre_i]\) 即可,即标记之和、标记的前缀最小值 和 标记的前缀最小值个数。容易验证这是封闭的。


CPU监控

区间加,区间赋值,查询区间历史最大值。

解答

节点信息:最大值和历史最大值。

此时我们的队列里既有赋值标记也有加法标记,跟上题一样,这是我们不愿意见到的。因此考虑转化。

容易发现,一个赋值标记后连续的加法标记都可以看作赋值标记!比如标记 \(=x\) 后面有标记 \(+y\),把标记 \(+y\) 看作标记 \(= x + y\) 即可。

这样,我们的队列就可以被分成两部分,连续的一段前缀都是加法标记,连续的一段后缀都是赋值标记。

与 线段树基础题 from cmd's blog 类似概括即可,只需要多记录后面赋值标记的 \(\max\) 这个信息就可以封闭。


这里举的几个例子仅为了感知构建概括的逻辑。优秀的题目还有很多,Reference(在文末)的两篇文章中均提到了更多的习题。

无法用懒标记处理的一般问题

此类问题通常在一般操作的基础上还含有区间最值操作。而吉司机线段树可以以均摊一个 \(\log\) 的代价把区间最值操作变为对区间最值的加减操作。

在此基础上,我们人为把每个节点的最值和非最值区分开来,也就是为每个节点维护两个概括(一个作用于最值,一个作用于非最值),这样就可以完成此类问题。

无区间最值操作的历史和问题

注意!此处的“历史和”和先前提到的“历史版本和”是不同的定义。

历史和问题大多都是询问 \(\sum B_i\)\(B_i\) 是单点的历史最大值或历史最小值或历史版本和)。

对于历史和问题,我们是通过转化(定义辅助数组)完成的。

注意,像区间赋值这样的操作属于第一类历史和问题(如 NOIp 2022 T4 比赛),这里无法支持区间赋值,只能支持变化量固定的操作。

下面都以区间加修改为例。

历史最大值的和

定义 \(C_i = A_i - B_i\)(对每一时刻都成立),当我们查询 \(\sum_{i=l}^r B_i\) 时,我们只要查询 \(\sum_{i = l}^r C_i\)\(\sum_{i=l}^r A_i\) 就可以了。

\(A_i\) 的维护就是普通线段树能完成的,那么我们如何维护 \(C_i\)

从定义上说,如果我们对 \(A_i + x\),那么新的 \(B_i \leftarrow \max(B_i, A_i + x)\)

故新的 \(C_i \leftarrow A_i + x - \max(B_i, A_i + x) = \min(A_i - B_i + x, 0) = \min(C_i + x, 0)\)

故我们先区间加再对区间 checkmin,这是吉司机线段树可以完成的。

历史最小值的和

与历史最大值的和类似,维护 \(C_i = A_i - B_i\),每次相当于 \(C_i = \max(C_i + x, 0)\)

我们先区间加再区间 checkmax,也是吉司机线段树可以完成的。

历史版本和的和

\(t\) 为当前的时刻(当前操作前的操作数),定义 \(C_i = B_i - t A_i\)

假设一次操作后 \(A_i \leftarrow A_i + x\),那么 \(B_i \leftarrow B_i + A_i + x\)(此处的 \(A_i\) 是修改前的)。

\(C_i \leftarrow B_i + A_i + x - (t+1)(A_i + x) = B_i - t A_i - t x = C_i - t x\)

因此 \(A_i\) 区间加 \(x\)\(C_i\) 就区间减 \(tx\),这是普通线段树就能维护的。

有区间最值操作的区间和问题

把第二类和第三类的做法拼一起!

我们通过支付一个 \(\log\) 的代价变为区间加减最值。

然后对于区间的最值和非最值分开做第三类问题即可。

扩展

吉司机线段树的一些问题是可以对多个(假设 \(k\) 个)数组做的。

此时我们对于每个数组都区分最值和非最值,时间复杂度大概 \(\mathcal O(2^k \times t)\)\(t\) 为原问题复杂度)。

Reference