线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 $O(\log N)$ 。而未优化的空间复杂度为 $2N$ ,因此有时需要离散化让空间压缩。
用途
在 $\sf\ O(\log N)$ 的时间复杂度内实现:
单点修改、区间修改、区间查询$\sf\ etc.$
线段树维护的信息需要满足可加性
,如果使用标记,标记也要满足可加性。
实现过程
基本结构与建树
有个数组 $\sf\ a={10,11,12,13,14}$ 要进行区间求和操作
怎么把这个数组存到线段树中呢?
设线段树的根节点编号为 $1$
用数组 $d$ 来保存我们的线段树
$d[i]$ 用来保存编号为 $i$ 的节点的值(这里节点的值就是这个节点所表示的区间总和)
如图所示:
设$\sf\ d[i]$是线段树中的结点。
$\sf\ d[i]$ 的左节点是 $\sf\ d[ls(i)]$
$\sf\ d[i]$ 的右节点是 $\sf\ d[rs(i)]$
如果 $\sf\ d[i]$ 表示的是区间 $\sf\ [l,r]$
$\sf\ d[i]$ 的左儿子节点表示的是区间 $\sf\ [l, \frac{l+r}{2} ]$
$\sf\ d[i]$ 的右儿子表示的是区间 $\sf\ [ \frac{l+r}{2} +1,r]$ 。
如何实现?
如图
1 |
|
关于线段树的空间:
如果采用$\sf\ 2p$ 是 $\sf\ p$ 的左儿子,$\sf\ 2p+1$ 是 $\sf\ p$ 的右儿子的存储方式(堆存储
),则$\sf\ d $数组的长度应为$\sf\ 2^{\left\lceil\log{n}\right\rceil+1}$,亦即取 $2$ 的幂中第一个大于等于 $\sf\ n$ 的幂并将其乘二作为$\sf\ d$数组的长度。
一般来说,开四倍是怎么样都够了的,但是在卡空间的题里要进行类似于动态开点的操作。
区间查询
区间查询,比如求区间 $\sf\ [l,r]$ 的和的操作。
比如查询区间$\sf\ [3,5]$,把 $\sf\ [3,5]$ 拆成 $\sf\ [3,3]$ 和 $\sf\ [4,5]$ 就行了。
1 | int getsum(int l,int r,int l_now,int r_now,int p) |
主要思路是把区间拆成左右区间,再分别处理。(分治)
区间修改&懒标记
修改区间$\sf\ [l , r]$需要进行打懒标记的操作来减少时间消耗。
设一个数组 $\sf\ laz$ , $\sf\ laz[i]$ 表示编号为 $\sf\ i$ 的节点的懒标记。
区间修改(区间加):
1 | void interval_add(int l,int r,int val,int l_now,int r_now,int p) |
区间查询(求和):
1 | int getsum(int l,int r,int l_now,int r_now,int p) |
如果要实现区间赋值,把所有 +=
替换成 =
即可
(除$sum+=getsum(l,r,mid+1,r,rs(p)$ )
1 | void interval_value(int l,int r,int val,int l_now,int r_now,int p) |
优化
位运算优化
递归到叶节点的时候叶节点一定包含在查询区间内,所以一定会在懒惰标记下放前就处理完$\sf\ return $,所以叶节点懒标记下放不会导致数组越界,也不用每次检查是否为叶节点了。
如果懒标记不会超出数据范围,那么可以将标记永久化,永久化可以避免下传标记,降低程序常数。
小练1.LuoGu3372:线段树1
小练2.LuoGu3373:线段树2