跳转至

线段树的扩展运用

在线段树本身信息动手脚的题一般就三步:

  1. 考虑怎么搞出具有幺半群性质的信息。(拆出多个信息(比如区间方差),或者想办法扩展一下信息来搞出结合律(比如 GSS⅓))
  2. 考虑怎么写 Pushup (假设左右儿子信息都已得到,然后看父亲节点怎么搞,边界为叶子节点)
  3. 考虑怎么写 Pushdown 和 Change。

如果是利用转化,把复杂的信息转化成简单的信息在线段树上维护的话,就只能靠灵感或者积累了。(比如区间加多项式一类的)

区间加多项式ψ(`∇´)ψ

区间加多项式都是转化成差分数组之后前缀和,所以一般都是单点查询。

区间查询看看可不可做先。

区间加等差数列ψ(`∇´)ψ

区间 \([l,r]\) 加等差数列,等同于在差分数组上的 \([l + 1,r]\) 做一次区间加 \(d\),然后令 \(c[l] + \text{BEGIN}\)\(c[r+1] - \text{END}\)

\(\text{BEGIN,END}\) 分别是首项和末项。

单点询问只需要询问线段树上的 \(sum(1,pos)\) 即可。

例题: CF1661D Progressions Covering

区间加斐波那契ψ(`∇´)ψ

这里鸽子了。

区间加等比数列ψ(`∇´)ψ

这里鸽子了。

区间查某些东西ψ(`∇´)ψ

区间 MEXψ(`∇´)ψ

最简单的方法是考虑莫队。

如果要求在线就直接上主席树(权值线段树二分显然可以做全局,然后可持久化之后能转成区间的)

还有一种离线+线段树的暂时咕咕咕着,反正不如主席树或者莫队做法。

区间方差ψ(`∇´)ψ

区间加法,区间询问方差。

考虑拆柿子:

定义平均数为 \(ava\)

方差的定义是 \(\dfrac{1}{n} \sum\limits_{i = 1}^{n} (a_i - ava)^2\)

然后显然这个非常不好维护(不具有幺半群的性质,线段树没法维护),所以考虑拆开,变成几个具有幺半群性质的信息的组合。

\[ \begin{aligned} \dfrac{1}{n} \sum\limits_{i = 1}^{n} (a_i - ava)^2 \\ \dfrac{1}{n} \sum\limits_{i = 1}^{n} (a_i^2 - a_i \cdot ava + ava^2)\\ \dfrac{1}{n} \cdot [\sum\limits_{i = 1}^{n} a_i^2 - ava\sum\limits_{i = 1}^n a_i + n \cdot ava^2] \end{aligned} \]

于是如果不带修就只需要考虑维护区间和,区间平方和就行了。

然后这里有一个区间加法。

所以我们考虑维护一个 \(add\)

显然区间加的时候和下放标记时,\(dat\) 的变化是一致的,就是考虑当区间每一个元素都被加上 \(v\) 之后,\(psum, sum\) 怎么变了。

然后区间和随便搞,考虑区间平方和 \(psum\)

\[ \sum\limits_{i = 1}^n(a_i + v)^2 - \sum\limits_{i = 1}^{n} a_i^2 \]

这个柿子很容易展开然后转化成能够 \(O(1)\) 下传,且具有时间轴上的结合律的信息,于是标记部分就做完了。

Pushup 很显然就是两个信息直接做加法,没了。

区间 sin 和ψ(`∇´)ψ

区间加区间 sin 和

思考好线段树的本质就行。

首先肯定要打 Tag,就是一个维护加法 Tag。

因为我要求的答案是 \(\sin\) 和,为了出现结合律,需要用和角公式。

所以信息只需要同时维护区间 \(\sin\) 和还有 \(\cos\) 和。

考虑一下区间加 \(v\) 之后一个区间的 \(\sin\) 怎么变了,方便下传标记还有更新信息。

先写出对于每一个元素的:

\(\sin (a + b) = \sin a \cos b + \sin b \cos a\)

\(\cos (a + b) =\cos a \cos b - \sin a \sin b\)

那么 \(\sum \sin(a_i)\) 就变成了 \(\sum \sin (a_i + v) = \sum (\sin a_i \cos v + \sin v \cos a_i)\)

展开:\(\cos v\sum \sin a_i + \sin v \sum \cos a_i\),此时 独立了变量(维护的信息)和常量 \(v\),很方便处理。

同理:\(\sum \cos (a_i) =\sum \cos(a_i + v) = \cos v\sum \cos a_i - \sin v \sum \sin a_i\)

注意到一个坑点就是 \(\cos\) 还要用到原来更新前的 \(\sum \sin a_i\),所以要提前记录一下。

然后 pushup 需要结合两个区间的 \(\sum \sin a_i, \sum \cos a_i\),直接加起来就可以了。

然后就没了。

区间 GCDψ(`∇´)ψ

单点修改ψ(`∇´)ψ

这个很简单,因为 \(\gcd\) 满足结合律 : \(\gcd(a, b, c) = \gcd(a, \gcd(b, c))\)

而且有幺元 \(0\)

所以直接做就行了。

区间修改ψ(`∇´)ψ

考虑转化成单点修改,直接维护差分数组,可以做到单点改。

然后发现 \(\gcd(a, b - a, c - b) = \gcd(a, b, c)\),所以直接询问差分数组的 \(\gcd\) 就行了。

然后显然对于一个询问 \([l, r]\) 还要用到 \(a[l]\),所以再开一个树状数组维护 \(a\) 就行了。

注意如果 gcd 的参数有负数就直接把 abs 带上去算就行。

扩展类ψ(`∇´)ψ

维护矩阵乘法ψ(`∇´)ψ

直接看这里就行了。

GSS 系列ψ(`∇´)ψ

GSS1 & 3 区间最大子段和ψ(`∇´)ψ

可以发现最大子段和没有明显的结合律。

不过我们试着假设左右儿子的最大子段和都已经求出,考虑怎么 Pushup。

那么其实也就三种情况,分别继承两边的,或者是左边取一段后缀,右边取一段前缀。

显然不可能有别的情况更优秀了。

所以需要维护一下每个子节点的最大前缀和,最大后缀和。

然后最大前缀和也需要考虑 Pushup,

显然就是左儿子的最大前缀和,或者是左儿子整体+右儿子的最大前缀和(因为子段是连续的,所以要选右儿子的最大前缀和就需要把左儿子整体选上)。

然后有没有一种可能,就是我右儿子选完最大前缀和之后,有没有可能后面还能找一些接到前缀后面比这个更大?

明显是蠢了,这里都是最大前缀和了,怎么可能嘛。

然后还有一种情况就是全部都选上。

最大后缀和同理即可。

注意如果一个区间都是负数,最大子段和应该是最大值,而不是上面三种情况的任意一种。

所以还需要维护一个区间最大值。

此时最大子段和就有结合律了。

GSS1 不带修,GSS3 带一个单点修,本质一样。

类区间最大子段和问题ψ(`∇´)ψ

你有一个序列 \(a\),一个序列 \(b\)

你每次需要求一个区间内的一种贡献的最大值,定义为 \(\max\{a_x + b_y\}, x \le y\)

这个东西其实也和最大子段和差不多,我们发现这个信息其实是可以合并的,我们直接维护答案,显然大区间的答案可能是两个小区间的答案之一,还有一种情况是从两个小区间各取一些组成答案。

所以对于每个节点,维护当前区间内最大的 \(a_x\) 和最大的 \(b_y\),每次合并的时候,因为左右子树维护的区间是分开的,所以 \(x\le y\) 的条件也可以被满足。

然后答案就可以上传了,复杂度一个 log,也可以支持单点修改。

GSS4 区间开方(下取整),区间求和ψ(`∇´)ψ

这题线段树也可以,虽然算是一个单独的 Trick,但毕竟是 GSS 系列的,就也写上了。

首先从数列分块入门 5 可以得到一个结论,就是区间开方的次数是很少的,如果 1e18 级别开 6 ~ 7 次就够了。

所以我们可以维护一个并查集,其中 \(pa(i)\) 表示,\([i, n]\) 之中,第一个不是 \(1\) 的数的下标。

初始指向自己。

然后对于每个询问我们直接暴力在 \(a(i)\) 上面单点开根号,如果遇到 \(1\) 就直接跳 \(pa(i)\),因为开根号次数很少,然后多次操作之后要操作的数也很少。

并查集基本可以看作均摊 \(O(1)\) 的,所以这个东西复杂度非常优秀。

然后区间和再用一个树状数组维护就可以了,因为我们已经把区间开方转化为了单点开方。

这个就是一个很有意思的 Trick:对于这种操作多了之后“无效操作”变多的操作,可以考虑使用并查集记录一下每一个位置的下一个有效操作位置,把区间操作转化为单点操作。

几个同类型的题: Luogu2391 白雪皑皑,CF920F Sum and Replace,51Nod1743 雪之国度。


最后更新: October 13, 2023