线段树(interval tree) 是把区间逐次二分得到的一树状结构,它反映了包括归并排序在内的很多分治算法的问题求解方式。
上图是一棵典型的线段树,它对区间[1,10]进行分割,直到单个点。这棵树的特点
是:
1. 每一层都是区间[a, b]的一个划分,记 L = b - a
2. 一共有log2L层
3. 给定一个点p,从根到叶子p上的所有区间都包含点p,且其他区间都不包含点p。
4. 给定一个区间[l; r],可以把它分解为不超过2log2 L条不相交线段的并。
其中第四点并不是很显然,图8.1显示了[2, 8]的分解方式,深灰色结点为分解后的
区间,浅灰色结点是递归分解过程中经过的结点。为了叙述方便,下面称树中的结点
所对应的区间为树中区间。
从第3点和第4点可以得出结论:修改一个点只用修改log2 L个树中区间信息,而统计一个区间只需要累加2log2 L个树中区间的信息,且访问的总结点数是O(log L)的。两个操作都很容易实现。
动态统计问题I 有一个包含n个元素的整数数组A,每次可以修改一个元素,也可以询问某一个区间[l; r]内所有元素的和。如何设计算法,使得修改和询问操作的时间复杂度尽量低?
方法一 直接做, 则修改操作是O(1)的, 但询问需要进行累加, 时间复杂度为O(r ? l),最坏情况为O(n)。
方法二 建立一棵线段树,每个树中区间记录该区间的元素和,则由刚才的结论,修改元素时只需要修改log2 L个树中区间的元素和,而统计时只需要累加2log2 L个树中区间的元素和,两个操作都是O(log n)的,比方法一好很多。
动态统计问题II 有一个包含n个元素的整数数组A,每次可以同时给一个区间[l; r]内的数同时增加一个数d(d可以为负),也可以询问某一个数Ai的值。如何设计算法,使得修改和询问操作的时间复杂度尽量低?
如何快速修改一个区间?修改一个树中区间[a; b]将影响到以[a; b]为根的整棵子树和它的所有祖先,所以如果沿用刚才的线段树定义,是不可能快速实现区间修改操作的。
解决方法是借用数据结构中常用的\懒操作"的思想,只记录有哪些操作需要执行,而不去真正执行这些操作。换句话说,在需要给树中区间[l; r]的元素同时增
加d时,我们只记录\曾经有一条指令add(l, r, d)"就可以了。我们把记录的这个值称为树中区间的add值,则叶结点的元素值为它所有祖先的add值之和。根据前面的结论,每一条任意区间的修改指令可以分解成2log2 L个树中区间的修改指令,且修改操作具有叠加性,因此修改操作的时间复杂度为O(log n)。查询操作只
需要累加叶子的所有祖先,它也是O(log n)的。
动态统计问题III 有一个包含n个元素的整数数组A,每次可以同时给一个区间[l; r]内的数同时增加一个数d(d可以为负),也可以询问一个区间[l; r]内所有元素的和。如何设计算法,使得修改和询问操作的时间复杂度尽量低?
显然前两个问题都是此问题的特殊情况,因此这个问题比前两个问题难度更大。注意到上一个问题线段树只能提供叶结点的真正元素和,因为对于任何一个树中区间[l; r]来说,影响它的指令所修改的区间不仅包括它的所有祖先,也包括它的所有后代。所以[l; r]内的所有元素和应该等于[l; r]的所有祖先的add值加上[l; r]所有后代的add值。
但后代有很多,直接累加的时间开销过大。这里需要再一次利用线段的树的区间相加性质,记录一个附加值sum_of_add,表示以[l; r]为根的子树上所有树中区间的add值之和。
修改操作把区间分解为树中区间,修改它们的add值,并且要顺便修改它们父亲的sum_off_add值。由于区间分解过程访问的总结点是O(log L)的,因此修改操作是O(log n)的。
查询操作把区间分解为树中区间,并统计它们所有祖先的add值之和,再与这些树中区间本身的sum_off_add相加即可。
文章出自《算法艺术与信息学竞赛》 ---- 作者:刘汝佳