线段树(segment tree)
线段树主要用于维护区间信息,与传统的树状数组相比,可以实现O(log n)
的区间修改,还可以同时支持多种操作(加、乘),更具通用性。
还是一样,为了方便测试,我们引入一个例题中的数据来演示。
【模板】线段树
题目链接:[线段树 1](#P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
==题目描述==
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 $k$。
- 求出某区间每一个数的和。
==输入格式==
第一行包含两个整数 $n, m$,分别表示该数列数字的个数和操作的总个数。
第二行包含 $n$ 个用空格分隔的整数,其中第 $i$ 个数字表示数列第 $i$ 项的初始值。
接下来 $m$ 行每行包含 $3$ 或 $4$ 个整数,表示一个操作,具体如下:
1 x y k
:将区间 $[x, y]$ 内每个数加上 $k$。2 x y
:输出区间 $[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
==提示==
对于 $30%$ 的数据:$n \le 8$,$m \le 10$。 对于 $70%$ 的数据:$n \le {10}^3$,$m \le {10}^4$。 对于 $100%$ 的数据:$1 \le n, m \le {10}^5$。
保证任意时刻数列中所有元素的绝对值之和 $\le {10}^{18}$。
【样例解释】
![]()
线段树的建立
线段树的每个节点对应一个区间,最小的区间(最底下的叶子)只包含1个数,从node=1(根节点)往下逐渐二分,且每一层的区间并不重合。
我们如此设置节点(简单概念):
每个节点p的左右子节点分别是2p和2p+1,假如节点p储存的是[l, r]的和,我们取mid=(l+r)/2 (一般就是向下取整啦,数组从索引0开始)则其子节点恰是分别储存[l, mid]和[mid+1, r]的和,可以发现这样可以使得做节点对应的区间长度恰好与右节点对应的区间长度相同或恰好多1。
草履虫都会的递归建图:
|
|
如何更新数据?
朴素的想法或许是在改变数组中的某值之后再次递归来同步线段树,就像建树过程一样,但可以预见的是,这样做的时间复杂度比较高,因为当数组中的某值改变,我们需要update的节点是叶子节点,修改一个点会需要连锁修改上方所有包含该点的父节点、祖节点…etc,所以我们引入==懒标记法==进行线段树的区间修改。
懒标记(延迟标记)
这是线段树的精髓所在qwq,递归的复杂度主要在于一层套一层然后再一层一层出来,有没有一种可能,我们给我们要修改的区间(将会对应一个唯一的节点)做上标记,只需要在将来用到某个节点的时候将变化传递下去就行。这样将会大大减少我们的时间。
相当于啊,本来某个变化是变动整个树来更新的,现在我们懒一懒,把工作做到确定哪些区间是全部变化了的,给它标记上(带上变化的内容),只有等到需要找它的子区间的时候才把变化落实下去。
还是看代码来理解吧:
|
|
整个过程看着还是有递归,但是并不像建树时一样递归到底层才结束,我们在中间随时可以结束,只要找到一个可以代表它下面的所有小弟的大哥,然后告诉大哥要这样…那样…然后我们的任务就完成了,意思已经传达到了。
那么如何把我们的标记翻译下去呢,这个就更简单了,我们上面使用了一个mark[]
数组记录变化,我们只需要吧mark里的指令传递到目标即可。
|
|
接下来,我们如果想要更新到所有叶子结点,只要调用这个落实函数就好。
查询区间
上面我们已经有了修改区间的方法,其实查询已经讲进去了吧,不会查怎么做修改
简单的写一个模板函数:
|
|
最后给上例题的参考代码:
|
|