线段树基本概念
线段树/区间树是一种非常常用的对区间数据进行操作的数据结构。
线段树是一棵二叉树(但不一定是完全二叉树!),它的每个节点均代表一个区间,且父节点代表的区间为左右子节点代表的区间之和。特别的,根节点代表的区间为所有节点代表区间之和,各个叶节点代表区间为单个点(即长度为1的区间)。
线段树结构
树中的每一个结点表示了一个区间[a,b]。 a,b通常是整数。每一个叶子节点表示了一个单位区间(长度为1)。对于每一个非叶结点所表示的结点[a,b],其左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b](除法去尾取整)。
如下图为区间[1, 9]的线段树:
线段树和区间
要用线段树解决区间问题,首先需要将区间对应到线段树的节点上。这就需要进行在线段树上进行区间分解。
(1)如果有某个节点代表的区间完全属于待分解区间,则该节点为“终止”节点,不再继续向下分解;
(2)所有“终止”节点所代表的区间都不重叠,且加在一起就恰好等于整个待分解区间;
(3)区间分解时,每层最多2个“终止”节点,所以“终止”节点的总数也是 log(n)量级的
线段树性质
- 线段树为二分构造,若根节点对应区间为[a,b],则其深度为 log2(b−a+1)(向上取整)。这样在进行更新查询操作的时候,操作的复杂度就可以为log(n)量级
- 叶子节点的数目和根节点表示的区间长度相同
- 若叶子节点的数目为N,则线段树的总节点数目为2*N-1。因为线段树的节点要么是0度,要么为2度,根据二叉树的性质可知。
- 若叶子节点为N,要想用连续的数组表示一棵线段树,则数组的大小应该为4N。
因为,根据性质3,线段树总节点数目为2*N-1,又由于线段树不是完全二叉树,因此其最低的叶子一层并不是紧靠最左边,这样在其倒数第二层上的索引号接近2N的位置,按照2*index+1和2*index+2的方式来访问其左右子节点,这就导致整个数组的大小要约为 4*N. - 线段树上,任何一个区间被分解后得到的“终止”节点的数目都是log(n)量级。
因为每一层最多有两个“终止”节点,共log(n)层,这样在线段树上进行更新叶子节点和进行区间分解的时间复杂度都是O(logn).
线段树操作的基本类型
(1)单点更新
每次对区间内的一个点进行更新,更新操作需要递归到叶节点进行操作。在向下递归的过程中,沿途经过的所有节点都要进行修改。
(2)成段更新
简单的说明:成段更新需要用到延迟标记(或者说懒惰标记),简单来说就是每次更新的时候 不要更新到底,用延迟标记使得更新延迟到下次需要更新or询问到的时候。 延迟标记的意思是:这个区间的左右儿子都需要被更新,但是当前区间已经更新了。
(3)区间合并
区间合并是在线段树查询的时候,对当前区间的左右儿子进行合并。
(4)扫描线
线段树解题的一般规律
- 首先要确定是一个区间的问题,如果不是明显的区间,看是否能转化为区间。例如 poj_3321的解答
- 要想清楚每个节点要存放哪些信息(当然,区间的起始和终止节点,以及左右子节点指针是必须的)
- 更新的时候,看是否能够lazy更新,即不要一下就更新到叶子节点,这样会导致更新效率降低
- 先建树,然后插入数据,再更新/查询