线段树的各种姿势
普通线段树
线段树可以维护序列。一棵线段树长这样:
1 2 3 4 5
|---------|
/ \
|-----|---|
/ \ / \
|---|-|-|-|
/ \
|-|-|
容易发现,线段树是一棵二叉树。线段树的每个节点都对应原序列的一段区间,且每个节点的左儿子和右儿子对应的区间长度几乎相等。
具体地说,假设当前节点对应区间 \([l,r]\),则其左儿子对应区间 \([l,\lfloor \dfrac{l+r}{2} \rfloor]\),右儿子对应区间 \([\lfloor \dfrac{l+r}{2} \rfloor+1,r]\)。如果 \(l=r\),说明当前节点为叶子节点。
考虑如何分配节点的编号,这里我们可以令根结点的编号为 \(1\),\(i\) 号节点的左儿子编号为 \(2i\),右儿子编号为 \(2i+1\),容易发现这样分配编号是对的,注意此时数组的空间需要开到 \(4n\),否则可能会出现越界等情况。
(待填)
懒标记与标记永久化
(待填)
非递归线段树
(待填)
单侧递归线段树
(待填)
区间最值操作
(待填)
可持久化线段树
(待填)
区间历史查询
先说历史版本和。
以区间加操作为例,我们需要维护一个长度为 \(n\) 的序列 \(a\) 和一个长度为 \(n\) 的初始全为 \(0\) 的序列 \(s\),维护以下三种操作:
-
首先 \(\forall i \in [l,r] , a_i \leftarrow a_i + x\),接着 \(\forall i \in [1,n] , s_i \leftarrow s_i + a_i\)
-
查询 \(\sum\limits_{i=l}^r a_i\)
-
查询 \(\sum\limits_{i=l}^r s_i\)
这里的 \(s_i\),也就是我们说的历史版本和,我们要做的就是在维护 \(a\) 的同时维护 \(s\),这里直接套用传统的线段树并不行,我们需要使用别的方法维护。
先来一个极端暴力的做法。我们考虑对于节点 \(i\) 需要维护什么信息,假设其对应的区间为 \([l_i,r_i]\),则我们一定需要维护 \(sum_i=\sum\limits_{i=l_i}^{r_i} s_i\) 以及 \(num_i=\sum\limits_{i=l_i}^{r_i} a_i\),考虑一次修改操作产生的影响,不妨令 \(len_i=r_i-l_i+1\),则一次区间加操作产生的影响为:
- 首先 \(num_i \leftarrow num_i+len_i \times x\),接着 \(sum_i \leftarrow sum_i + num_i\)。
我们不妨记修改后的数为 \(sum_i'\) 和 \(num_i'\),则可以得到:
-
\(num_i' = num_i + len_i \times x\)
-
\(sum_i' = sum_i + num_i + len_i \times x\)
发现这个东西容易用矩阵维护。对于节点 \(i\) 考虑维护一个矩阵 \(\begin{bmatrix} sum_i & num_i & len_i\end{bmatrix}\),则一次区间修改容易用下面的式子刻画:
我们只需要维护一棵支持区间乘矩阵的线段树即可,这一点是简单的。
但是我们还有一个问题:对于没有进行修改的区间,其历史和不会被更新,这个时候我们需要手动更新。你当然可以选择区间加 \(0\),但是这种方法的适配性不高。我们采用下面的式子:
欸你这个怎么和区间加 \(0\) 一模一样啊!但是不同的是,对于所有没有操作到的位置,都可以用这个矩阵转移。
于是我们就解决了这个问题,复杂度 \(O(3^3 n \log n)\)。
当然这个做法是我口胡的。
但是不要紧,我们来看新的问题,现在我们需要维护一个长度为 \(n\) 的序列 \(a\) 和一个长度为 \(n\) 的初始全为 \(0\) 的序列 \(s\),维护以下三种操作:
-
首先 \(\forall i \in [l,r] , a_i \leftarrow x\),接着 \(\forall i \in [1,n] , s_i \leftarrow s_i + a_i\)
-
查询 \(\sum\limits_{i=l}^r a_i\)
-
查询 \(\sum\limits_{i=l}^r s_i\)
你考虑此时的操作形如:
-
\(num_i' = len_i \times x\)
-
\(sum_i' = sum_i + len_i \times x\)
接下来直接把矩阵的维护往里面套一下即可。
当然对于没有修改的位置是这样的:
于是你得到了一个口胡的大常数做法。
当然你可以在矩阵具有特殊性质的时候采用特殊方法维护,仍然以区间加为例。你考虑如下的矩阵乘法。
规律应该比较明显,所以我们只需要对于一个节点维护矩阵左下方的三个值即可,常数骤减,根本不要按照别的方法一样考虑标记的转移顺序,这很有道理!
那接下来考虑区间历史最值问题,我们以这道题为例,区间加与区间覆盖不变,但查询的是区间最大值与区间历史最大值,自然地我们考虑把矩阵乘法换成 \(\{max,+\}\) 的矩阵乘法。
我们考虑对于每个节点 \(i\),维护 \(\begin{bmatrix} maxn_i & num_i & 0 \end{bmatrix}\),接下来的两种操作可以描述:
-
区间加操作,\(num_i' = num_i + x\),\(maxn_i' = \max(maxn_i,num_i + x)\)
-
区间覆盖操作,\(num_i' = x\),\(maxn_i' = \max(maxn_i,x)\)
我们写成矩阵乘法形式,对于区间加:
对于区间覆盖:
对于没有操作的区间:
于是就结束了。

浙公网安备 33010602011771号