【算法】线段树详解

前言

这是一篇蒟蒻的博客,可能有许多错误或不详细的地方,欢迎大佬们指出。
这篇文章主要参考了这篇博文: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] 的答案合并得到。
经典的区间加法问题有:

  1. 区间求和( ∑ 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,LM<R);
  2. 区间最大值( 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),LM<R)。

不满足区间加法的问题有:

  1. 区间的众数;
  2. 区间的最长不下降子序列。

线段树的原理及实现

注意:如果我没有特别申明的话,这里的询问全部都是区间求和
线段树主要是把一段大区间 平均地划分 成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在 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] 的线段树的分解过程(相同颜色的节点在同一层)

可以发现,这棵线段树的最大深度不超过 ⌊ log ⁡ 2 ( n − 1 ) ⌋ + 2 \lfloor\log_2(n-1)\rfloor+2 log2(n1)⌋+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}]

  • 238
    点赞
  • 727
    收藏
    觉得还不错? 一键收藏
  • 52
    评论
评论 52
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值