线段树作为一种十分常用的数据结构,在NOIP、NOI中广泛的出现
线段树支持对一个数列的求和、单点修改、求最值(最大、最小)、区间修改。这几种操作,时间复杂度是(logn)级别的。
具体的操作一般是每次对数轴上的一个区间或是数列中的连续若干个数进行一种相同的处理
一.引例
示例1.区间最值问题
给定一个n(n <= 100000)个元素的数组A,有m(m <= 100000)个操作,共两种操作:
1、Q a b 询问:表示询问区间[a, b]的最大值;
2、U a c 更新:表示将第a个元素变成c;
示例2.区间求和问题
给定一个n(n <= 100000)个元素的数组A,有m(m <= 100000)个操作,共两种操作:
1、Q a b 询问:表示询问区间[a, b]的元素和;
2、A a b c 更新:表示将区间[a, b]的每个元素加上一个值c
若是使用一般方法,循环得出结果的话,会出现O(nm)的时间复杂度,复杂度太高,
线段树用了区间拆分的思想,解决了上述问题
二.线段树的基本概念
1.二叉搜索树
如图2-1:
- 线段树是一种二叉搜索树,即每个结点最多有两棵子树的树结构,字树被称作“左子树”和“右子树”
- 每个节点存储的是一个区间,但这个区间又不是存储属于该区间的所有的数,仅仅是左右端点
- 所有的叶子节点表示的是单位区间(左右值相等),所有的非叶子结点,都有左右两棵子树
图2-1
2.数据域
- 线段树的每个节点代表一个区间,所以区间的左右端点是数据域中很重要的一块
- 再者以求得区间的最大值为例,每个结点除了需要知道所管辖的区间范围[l, r]以外,还需要存储一个当前区间内的最大值max
图2-2
如上图2-2所示,该图表示一个数组A[1~6] = [1 7 2 5 6 3],叶子结点的max域为数组对应下标的元素值,非叶子结点的max域则通过自底向上的计算由两个儿子结点的max域比较得出。一般情况我们会进行2种操作:查询最大值和更新值。
(1.)查询最大值
我们一定可以将这个区间拆分成log(n)个子区间,并且这些子区间一定都能在线段树的结点上找到(这一点下文会着重讲解),然后只要比较这些结点的max域,就能得出原区间的最大值了,因为子区间数量为log(n),所以时间复杂度是O( log(n) )。
(2.)更新值
更新数组某个元素的值时我们首先修改对应的叶子结点的max域,然后修改它的父结点的max域,以及祖先结点的max域,换言之,修改的只是线段树的叶子结点到根结点的某一条路径上的max域,又因为树高是log(n),所以这一步操作的时间复杂度也是log(n)的。
3.结构体表示
在使用线段树过程中,我们有两种结构体表示节点的方法:指针和数组
(1.)指针表示
每个结点可以看成是一个结构体指针,由数据域和指针域组成,其中指针域有两个,分别为左儿子指针和右儿子指针,分别指向左右子树;数据域存储对应数据,根据情况而定(如果是求区间最值,就存最值max;求区间和就存和sum),这样就可以利用指针从根结点进行深度优先遍历了。
以下是简单的线段树结点的C++结构体:
struct treeNode {
Data data; // 数据域
treeNode *lson, *rson; // 指针域
}*root;
(2.)数组表示
基于数组的静态表示法,需要一个全局的结构体数组,每个结点对应数组中的一个元素,利用下标索引。
例如,假设某个结点在数组中下标为p,那么它的左儿子结点的下标就是2*p,右儿子结点的下标就是2*p+1(类似于一般数据结构书上说的堆在数组中的编号方式),这样可以将所有的线段树结点存储在相对连续的空间内。之所以说是相对连续的空间,是因为有些下标可能永远用不到。
以长度为6的数组为例,如图2-3所示,红色数字表示结点对应的数组下标,由于树的结构和编号方式,导致数组的第10、11位置空缺。
图2-3
struct treeNode {
Data data; // 数据域
int pid; // 数组下标索引
int lson() { return pid << 1; }
int rson() { return pid<<1|1; } // 利用位运算加速获取子结点编号
}nodes[ MAXNODES ];//线段树要开4倍的点的个数
三,基本操作
线段树的基本操作包括:构建线段树,查询,更新
(1.)构建线段树
线段树的构造是一个二分递归的过程,封装好了之后代码非常简洁,总体思路就是从区间[1, n]开始拆分,拆分方式为二分的形式,将左半区间分配给左子树,右半区间分配给右子树,继续递归构造左右子树。
(2.)更新
线段树的更新是指更新数组在[x, y]区间的值,具体更新这件事情是做了什么要根据具体情况而定,可以是将[x, y]区间的值都变成val(覆盖),也可以是将[x, y]区间的值都加上val(累加)。
更新过程采用二分,将[1, n]区间不断拆分成一个个子区间[l, r],当更新区间[x, y]完全覆盖被拆分的区间[l, r]时,则更新管辖[l, r]区间的结点的数据域
(3.)查询
线段树的询问和更新类似,大部分代码都是一样的,同样是将大区间[1, n]拆分成一个个小区间[l, r],这里需要存储一个询问得到的结果ans,当询问区间[x, y]完全覆盖被拆分的区间[l, r]时,则用管辖[l, r]区间的结点的数据域来更新ans