计算几何中的线段树(Segment Tree)
首先,我注意到“线段树”不在你提供的已讲词条列表中,所以我来详细讲解它。这是一个在计算几何和算法设计中用于高效处理区间查询的经典数据结构。
第一步:基本动机与问题引入
想象你有一个一维数轴,上面有N个点(或更常见的是,N个单位区间,每个单位区间对应一个数组元素)。你面对两类操作:
- 查询:频繁地询问某个给定区间
[L, R]上所有元素的某种“聚合信息”,比如求和、最小值、最大值、乘积等。 - 更新:偶尔修改数组中某个位置的值。
最朴素的方法是,每次查询都遍历区间[L, R]进行计算,时间复杂度是O(R-L+1),即O(N)。如果查询非常频繁,这效率很低。线段树的核心思想是预处理和存储一些中间结果,以便在O(log N)时间内回答查询,同时也能在O(log N)时间内完成单点更新。
第二步:核心思想——二叉树分解
线段树将一个大的区间(通常是整个数组范围[0, N-1])递归地二分成更小的子区间,并把这些区间组织成一棵二叉树。
- 树的根节点代表整个区间
[0, N-1]。 - 每个内部节点代表一个区间
[l, r]。它有两个子节点:- 左子节点代表左半区间
[l, mid],其中mid = (l+r)/2(向下取整)。 - 右子节点代表右半区间
[mid+1, r]。
- 左子节点代表左半区间
- 叶子节点代表长度为1的区间,即原始的单个数组元素。
第三步:节点存储的内容
每个树节点不仅仅记录它代表的区间范围[l, r],更重要的是,它存储了该区间上我们关心的聚合信息。例如,如果我们关心区间和,那么节点就存储其对应区间的元素和;如果关心区间最小值,就存储最小值。这个值是基于其子节点的值计算(或“聚合”)出来的。
- 叶子节点的值就是数组元素本身。
- 内部节点的值 = 其左子节点的值
op其右子节点的值,其中op是聚合操作(如加法、求最小值等)。
示例:数组A = [1, 3, 5, 7, 9, 11],构建求区间和的线段树。
根节点(区间[0,5], 和=36)
/ \
[0,2]和=9 [3,5]和=27
/ \ / \
[0,1]和=4 [2,2]和=5 [3,4]和=16 [5,5]和=11
/ \ / \
[0,0] [1,1] [3,3] [4,4]
1 3 7 9
第四步:查询操作(Query)
查询一个区间[L, R]的聚合值,从根节点开始递归:
- 如果当前节点区间
[l, r]完全包含在查询区间[L, R]内,则直接返回该节点存储的聚合值。这是我们效率提升的关键:不需要再深入它的子节点。 - 否则,计算中点
mid。 - 如果查询区间与左子区间
[l, mid]有重叠,则递归查询左子树。 - 如果查询区间与右子区间
[mid+1, r]有重叠,则递归查询右子树。 - 将左右子树的查询结果用
op操作聚合,返回。
由于每次递归,区间要么被完全包含(直接返回),要么被分成两部分,而分下去的区间不会同时与左右子区间都重叠(因为查询区间是连续的),所以递归深度是O(log N)。
第五步:更新操作(Update)
更新数组位置i的值,同样从根节点开始递归,找到代表[i, i]的叶子节点,更新其值。然后,在递归返回的路上,沿着从叶子到根的路径,更新所有祖先节点的聚合值。因为树的深度是O(log N),所以更新操作的复杂度也是O(log N)。
第六步:扩展到更高维度与区间修改
基础的线段树处理一维区间和单点更新。其重要变体包括:
- 延迟传播(Lazy Propagation):用于高效处理“区间更新”(如将整个区间
[L, R]的值都加上某个数)。其思想是,更新操作并不立即应用到所有叶子节点,而是“懒标记”在内部节点上,记下“这个区间需要进行的修改”。只有当后续查询或更新需要深入到其子节点时,才将标记下推。这使得区间更新也能在O(log N)内完成。 - 二维线段树:可以看作“线段树的线段树”,用于处理平面上的矩形区域查询。在X轴方向建一棵线段树,其每个节点又存储了一棵在Y轴方向上的线段树。复杂度通常为
O(log^2 N)。 - 动态开点线段树:当区间范围非常大(如
[1, 10^9])但实际有值的点稀疏时,并不预先建立完整的树结构,而是在更新和查询过程中按需创建节点,节省空间。
总结:
线段树是一个利用二叉树结构对区间进行分治预处理的数据结构。它将一个区间上的聚合信息存储在对应的节点中,通过查询时合并“完全包含”的子区间结果,实现了O(log N)时间的区间查询和单点/区间更新。它是解决一系列区间问题(如动态范围查询、区间赋值、扫描线算法等)的强大基础工具。其核心优势在于在查询、更新和空间复杂度之间取得了优秀的平衡。