目录
前言
这是一篇蒟蒻的博客,可能有许多错误或不详细的地方,欢迎大佬们指出。
这篇文章主要参考了这篇博文:http://blog.csdn.net/zearot/article/details/48299459
什么是线段树
线段树,是一种 二叉搜索树 。它将一段区间划分为若干 单位区间 ,每一个节点都储存着一个区间。它 功能强大 ,支持区间求和,区间最大值,区间修改,单点修改等操作。
线段树的思想和分治思想很相像。
线段树的每一个节点都储存着一段区间 [ L . . R ] [L..R] [L..R] 的信息,其中 叶子节点 L = R L=R L=R 。它的大致思想是:将一段大区间平均地划分成 2 2 2 个小区间,每一个小区间都再平均分成 2 2 2 个更小区间……以此类推,直到每一个区间的 L L L 等于 R R R (这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。
这样一来,每一次单点修改、单点查询的时间复杂度都只为 O ( log 2 n ) O(\log_2n) O(log2n) 。
但是,可以用线段树维护的问题必须满足 区间加法 ,否则是不可能将大问题划分成子问题来解决的。
什么是区间加法
一个问题满足区间加法,仅当对于区间 [ L , R ] [L,R] [L,R] 的问题的答案可以由 [ L , M ] [L,M] [L,M] 和 [ M + 1 , R ] [M+1,R] [M+1,R] 的答案合并得到。
经典的区间加法问题有:
- 区间求和( ∑ i = L R a i = ∑ i = L M a i + ∑ i = M + 1 R a i , L ≤ M < R \sum_{i=L}^Ra_i=\sum_{i=L}^Ma_i+\sum_{i=M+1}^Ra_i\quad ,L\leq M<R ∑i=LRai=∑i=LMai+∑i=M+1Rai,L≤M<R);
- 区间最大值( max i = L R a i = max ( max i = L M a i , max i = M + 1 R a i ) , L ≤ M < R \max_{i=L}^Ra_i=\max(\max_{i=L}^Ma_i,\max_{i=M+1}^Ra_i)\quad ,L\leq M<R maxi=LRai=max(maxi=LMai,maxi=M+1Rai),L≤M<R)。
不满足区间加法的问题有:
- 区间的众数;
- 区间的最长不下降子序列。
线段树的原理及实现
注意:如果我没有特别申明的话,这里的询问全部都是区间求和
线段树主要是把一段大区间 平均地划分 成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在 log \log log 级别(因为这棵线段树是平衡的)。也就是说,一个 [ L , R ] [L,R] [L,R] 的区间会被划分成 [ L , ⌊ L + R 2 ⌋ ] \left[L,\left\lfloor\frac{L+R}{2}\right\rfloor\right] [L,⌊2L+R⌋] 和 [ ⌊ L + R 2 ⌋ + 1 , R ] \left[\left\lfloor\frac{L+R}{2}\right\rfloor+1,R\right] [⌊2L+R⌋+1,R] 这两个小区间进行维护,直到 L = R L=R L=R 。
下图就是一棵 [ 1 , 10 ] [1,10] [1,10] 的线段树的分解过程(相同颜色的节点在同一层)
![](https://i-blog.csdnimg.cn/blog_migrate/aeb19ace38a8074bddadef71a3bad7a7.png)
可以发现,这棵线段树的最大深度不超过 ⌊ log 2 ( n − 1 ) ⌋ + 2 \lfloor\log_2(n-1)\rfloor+2 ⌊log2(n−1)⌋+2 。
储存方式
通常用的都是 堆式储存法 ,即编号为 k k k 的节点的左儿子编号为 2 k 2k 2k ,右儿子编号为 2 k + 1 2k+1 2k+1 ,父节点编号为 ⌊ k 2 ⌋ \left\lfloor\frac{k}{2}\right\rfloor ⌊2k⌋ ,用 位运算 优化一下,以上的节点编号就变成了 k<<1
, k<<1|1
, k>>1
。其它储存方式请见 指针储存和动态开点 。
通常,每一个线段树上的节点储存的都是这几个变量:区间左边界,区间右边界,区间的答案(这里为区间元素之和)
注意:线段树的大小其实是 5n 左右的。
下面是线段树的定义:
struct node
{
int l/*区间左边界*/,r/*区间右边界*/,sum/*区间元素之和*/,lazy/*懒惰标记,此处默认是区间加,下文会提到*/;
node(){
l=r=sum=lazy=0;}//给每一个元素赋初值
}a[N];//N为总节点数
inline void update(int k)//更新节点k的sum
{
a[k].sum=a[k*2].sum+a[k*2+1].sum+a[k].lazy;
//很显然,一段区间的元素和等于它的子区间的元素和
//如果有懒惰标记的话要相应地改变(记得加上懒惰标记的值!!!)感谢duck_master的指正
}
初始化
常见的做法是遍历整棵线段树,给每一个节点赋值,注意要递归到线段树的叶节点才结束。
void build(int k/*当前节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/)
{
a[k].l=l,a[k].r=r;
if(l==r)//递归到叶节点
{
a[k].sum=number[l];//其中number数组为给定的初值
return;
}
int mid=(l+r)/2;//计算左右子节点的边界
build(k*2,l,mid);//递归到左儿子
build(k*2+1,mid+1,r);//递归到右儿子
update(k);//记得要用左右子区间的值更新该区间的值
}
单点修改
当我们要把下标为 k k k 的数字修改(加减乘除、赋值运算等)时,可以直接在根节点往下DFS。如果当前节点的左儿子包含下标为k的数(即对于左儿子区间 [ L l s o n , R l s o n ] [L_{lson},R_{lson}]