线段树
线段树
引入
顾名思义,线段树就是由一堆线段组成的树,它的每个节点都维护一段区间的信息,而叶子节点维护的则是一个点的信息。例如:
假如说我们想维护区间和的话,那么根节点就代表所有区间的和,而其它节点则代表部分区间的和。由上图我们还能发现一个性质:节点i的权值 = 左儿子的权值 + 右儿子的权值。根据这个性质,我们很自然地想到可以递归建树,我们考虑用一个结构体存储树,即:
1 | struct tree |
可能这时有人会问:为什么内存要开四倍啊?经验之谈~才不会告诉你是笔者太蒟了不会推~ 关于这点,网上有很多大佬给了详细过程,有兴趣可以去查查。
线段树的功能
我们先来看一道题
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 ,分别表示该数列数字的个数和操作的总个数。
第二行包含 个用空格分隔的整数,其中第 个数字表示数列第 项的初始值。
接下来 行每行包含 或 个整数,表示一个操作,具体如下:
1 x y k
:将区间 内每个数加上 。2 x y
:输出区间 内每个数的和。输出格式
输出包含若干行整数,即为所有操作 2 的结果。
样例 #1
样例输入 #1
1
2
3
4
5
6
7 5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4样例输出 #1
1
2
3 11
8
20
对于这道题,如果我们使用朴素的方法的话,每次操作的时间复杂度为 ,而总共有 次操作,所以总的时间复杂度为 。对于这么简单的操作,这个复杂度我们显然是不能接受的,而如果能将每次操作的复杂度降低到 ,这下子速度就比较可观了。于是,线段树,启动!
区间修改
既然我们想降低复杂度,那么我们肯定不能枚举区间的每个值进行修改,而是对整个区间进行修改,而我们的线段树正好维护区间和,于是,如果线段树的一个节点维护的区间正好在目标区间里面,我们就可以直接修改这个区间,即tr[i].sum = k * (tr[i].r - tr[i].l + 1)
,意思就是将区间内每一个数加 。
那么如果这个节点所维护的区间不完全包含在目标区间里面呢?这时候我们遍可以缩小节点维护的区间范围,即递归这个节点的左右子树,于是乎,我们得到了区间修改的规则:
- 如果节点维护的区间完全包含在目标区间中,则修改该节点的
- 如果该节点的左子树与目标节点有交集,则搜索左子树
- 如果该节点的右子树与目标节点有交集,则搜索右子树
代码也不难实现啦:
1 | void update(int i, int l, int r, int k) |
这个时候可能有人要问了:为什么这样子的操作复杂度是 级别的呢?有能力的读者可以自行思考,这个问题呢我们最后再解释。~别急~
区间查询
对于区间查询,其实跟区间修改的规则差不多,如果节点的区间完全包含在目标区间里,就返回该区间的值,如果左子树有交集就搜索左子树,右子树有……诶等会儿,我们刚刚在修改区间的时候,一个区间被修改了,它的子区间并不会被修改,那如果我们查询到它的子区间了,岂不是查询到了修改前的结果?这代表我们之前美妙的想法失败了?这可不行,我们要想办法补救一下。
试想一下,假如我们在查询子树的时候,提前知道了这个区间被修改了,不就可以了吗。所以,我们在修改一个区间的时候,可以给它打一个标记,代表这个区间修改的值,在查询其子树的时候,再把这个标记带给子树,以便下一次查询就ok啦!这个标记也是线段树的精髓,其名为懒惰标记
代码也很简单啦:
1 | int query(int i, int l, int r) |
懒标记下放
现在我们的核心问题就是如何把懒标记放给子树,其实也很简单,因为我们的操作都是递归的,所以我们只需要让子树的状态与当前状态一致即可,而我们现在的状态是什么呢,就是懒标记存放着数,且所维护的值被修改。所以,我们只需要让子树的懒标记也存放数,且修改其维护的值即可。
代码如下:
1 | void pushdown(int i) |
现在我们就可以实现一棵完整的线段树啦,撒花撒花
复杂度分析
最后的最后,我们还是要解决开始的问题,为什么线段树每次的操作一定是 呢?不难发现我们时间的花费都在递归步上,我们现在来分析递归步的时间复杂度。假设当前节点为 ,查询区间为 ,于是可以分为以下情况:
l <= tr[i].l <= tr[i].r <= r
此时直接返回,不会再递归tr[i].l <= l <= tr[i].r <= r
或者l <= tr[i].l <= r <= tr[i].r
此时虽然会递归左右子树,但是其中一棵递归一次后就会变为情况一,所以实际上只有一棵子树会继续递归tr[i].l <= l <= r <= tr[i].r
这里又分两种情况:l <= r <= mid
或者mid <= l <= r
这种情况有一棵子树与目标区间没有交集,不会再产生递归l <= mid <= r
这种情况会递归产生两棵子树,且都会继续参与递归
可以发现,在上述递归步中,只有最后一种是真正产生了两棵子树,而最后一种结束后又会返回第二种,即不会再产生更多子树。所以实际上参与递归的最多就两条路,而线段树的深度为 ,于是每次操作的时间复杂度最多为 ,即 级
总结
到这里,线段树的基础内容就完全结束啦。当然,上述介绍的内容只是线段树最基础的内容,更多更丰富的线段树,就交给读者自行探索咯~才不会告诉你是因为我懒~