线段树(Segment Tree)是一种用来维护区间的数据结构。
与树状数组相比,线段树可以实现时间复杂度在 级别的区间修改,还可以同时支持多种操作(加、乘、最值等)。
#操作列表
- 上传(pushup)
- 建树(build)
- 下放懒标记(pushdown)
- 区间查询(query)
- 区间修改(modify)
#通用操作
#存储线段树
线段树是一个典型的二叉树,因此我们可以使用一个数组来存储线段树。
分析:很容易就知道线段树的深度为 ,可得线段树的节点个数为 ,粗略估计开大小为 的数组即可(可以使用位运算写成 n << 2
)。
1 |
|
变量名 | 用途 |
---|---|
l | 区间的左端点 |
r | 区间的右端点 |
s | 区间和 |
d | 懒标记 |
#上传(pushup)
之所以把上传放在建树前面说,是因为建树的时候要用到它。
1 |
|
将两个子节点所代表的区间的和相加即为父区间的和。
#建树(build)
1 |
|
先初始化当前区间,接下来分两种情况:
- 若当前区间长度等于 ,则直接将当前区间的区间和赋值为
a[l]
即可。 - 若当前区间长度大于 ,则将区间平均分成两部分(即从 处断开分为两个区间,可写作
l + r >> 1
),继续向下递归建立左右子树即可。
需要注意的是两个子区间没有交集,因此左子树的左端点是 、右端点是 ,右子树的左端点是 、右端点是 。
#区间查询(query)
1 |
|
- 如果这个区间被包含,直接返回该区间的和。
- 如果和左儿子区间有交集,则继续向左儿子区间递归查询。
- 如果和右儿子区间有交集,则继续向右儿子区间递归查询。
需要注意的是在递归查询左右儿子区间之前要先下放懒标记(pushdown),否则会出问题。
#区间加
本部分以 洛谷 P3372 【模板】线段树 1 为例子来简述一下线段树区间加的实现。
#下放懒标记(pushdown)
1 |
|
这部分代码其实很简单。
将左、右子树的懒标记加上父节点的懒标记,区间和加上 ( 分别表示儿子区间的左、右端点,表示父节点的懒标记),最后清空父节点的懒标记即可。
#区间修改(modify)
1 |
|
区间修改和区间查询的实现相似。
- 如果当前区间被包含,直接添加懒标记并修改区间和。
- 如果和左儿子区间有交集,则继续向左儿子区间递归修改。
- 如果和右儿子区间有交集,则继续向右儿子区间递归修改。
需要注意的是在递归修改左右儿子区间之前要先下放懒标记(pushdown),修改完成以后要上传新信息(pushup),否则会出问题。
#区间加、乘
本部分以 洛谷 P3373 【模板】线段树 2 为例子来简述一下线段树区间加、乘的实现。
在编写之前,结构体中需要先添加一个乘法的懒标记 x
,并将其赋初值为 ,修改之后的结构体如下所示。
1 |
|
#下放懒标记(pushdown)
1 |
|
此处遵循先乘后加的原则,先修改区间和,再修改乘法懒标记,最后修改加法懒标记,不要忘记 。
注意:此处清除懒标记的时候,乘法懒标记应修改为 。
#区间修改(modify)
1 |
|
大体上和加法的修改函数一样,而在修改时与下放懒标记做法相同,遵循先乘后加的原则。
调用的时候若只需要使用乘法部分,加数设置为 即可。若只需要使用加法部分,乘数设置为 即可。
#全部代码
到这里基本操作就说完了,下面是全部的 AC 代码。
#区间加
1 |
|
#区间加、乘
1 |
|