Segment Tree
动机
询问
给你一个数组, 如何快速计算任何一段连续子数组的元素和?
对于一个子数组来说, 如果遍历子数组的每个数, 把它们加起来, 时间复杂度是, 太慢了.
下标从到的子数组元素和, 可以看成是下标从1到right的子数组元素和, 减去下标从1到left-1的子数组元素和. 例如数组, 子数组的元素和, 等于的元素和, 减去的元素和.
按照这个方法, 算出每个前缀 (表示下标从到的连续子组)的元素和, 就可以地计算任意连续子数组的元素和了.
更新
但是, 如果还可以修改数组中的元素呢?
比如我把下标为1的元素修改了, 由于所有前缀都包含下标1, 那么就需要更新所有前缀的元素和, 更新操作就需要的时间, 这太慢了.
能不能把前缀拆分成若干段连续子数组呢?
如果拆分得太细, 比如拆分成, 虽然更新是的, 但计算子数组元素和还是得遍历累加, 时间复杂度是, 太慢了.
平衡
上面的做法, 要么询问是更新是, 要么询问是更新时, 时间差距悬殊.
如何平衡询问和更新的时间复杂度?
关键在于如何拆分子数组(区间).
能否把任意前缀拆分成若干个关键区间, 使得更新操作也只会更新若干个关键区间?
这样回答询问时, 只需要遍历并累加若干个关键区间的元素和.
如何拆分?
启示: 如果把一个正整数拆分成若干个不同的2的幂(从大到小), 那么只会拆分个数. 前缀能否也这样拆分呢?
举个例子, , 那么前缀可以拆分成三个长度分别为的关键区间: .
按照这个规则, 来看看从到是如何拆分的:
$$
& [1,1] = [1,1] \ (1 = 1) \
& [1,2] = [1,2] \ (2 = 2) \
& [1,3] = [1,2] + [3,3] \ (3 = 2 + 1) \
& [1,4] = [1,4] \ (4 = 4) \
& [1,5] = [1,4] + [5,5] \ (5 = 4 + 1) \
& [1,6] = [1,4] + [5,6] \ (6 = 4 + 2) \
& [1,7] = [1,4] + [5,6] + [7,7] \ (7 = 4 + 2 + 1) \
& [1,8] = [1,8] \ (8 = 8)
$$
数一数, 按照这种拆分方式, 总共有多少个不同的关键区间? (提示: 把注意力放在区间的右端点上).
有个: .
一般地:
- 如果是的幂, 那么无需拆分.
- 如果 不是的幂, 那么先拆分一个最小的的幂, 记作 (例如拆分出), 得到长为的关键区间, 问题转换成的 如何拆分, 这是一个规模更小的子问题.
总共有个不同的关键区间.
证明: 按顺序拆分前缀, 每次只会恰好拆出一个新的关键区间 (注意)之前拆分过了, 不会产生新的关键区间), 所以一共有个不同的关键区间.
算法
由于关键区间的右端点互不相同, 我们可以把右端点为的关键区间的元素和保存在中.
按照如下方法计算前缀的元素和:
- 初始化元素和.
- 每次循环, 把加到中, 对应关键区间的元素和.
- 然后更新 为, 表示接下来要拆分, 获取其中关键区间的元素和.
- 循环直到 为止.
- 返回.
由于正整数的二进制长度是