已发布·持续改进
Algorithm
线段树能在 O(log n) 时间内完成数组的区间求和、区间最小值查询与单点修改;若只需求和,树状数组是更轻量的选择。
线段树是一棵二叉树,它把数组不断对半划分,并为每个得到的区间保存和、最小值或最大值等汇总值。根节点负责整个数组,叶节点负责单个元素,每个内部节点保存两个子节点合并后的结果。因此,任何关于连续区间的问题都可以通过合并少数几个节点来回答。
前缀和可以在 O(1) 时间内求出区间和,但只要修改一个元素就需要 O(n);直接使用数组时修改很快,但每次查询都要 O(n)。线段树把单点修改和区间查询都控制在 O(log n),适合数值不断变化、区间查询又源源不断的场景。加上懒标记(延迟传播)后,它还能在 O(log n) 内完成给整个区间加上某个值这样的区间修改。
建议先弄清前缀和的局限,再用一个小数组手动画出线段树,跟踪一次查询和一次修改分别经过哪些节点。接着亲手写出简短的迭代版实现和树状数组,并用区间最小值、逆序对计数、区间加与区间求和等经典题目反复练习,就能在算法面试和竞赛中熟练运用。
每个节点负责数组中的一个区间并保存其和或最小值;只要运算满足结合律并有单位元,都可以放进同一结构。
修改一个元素只需重新计算从叶到根的一条路径,区间查询在每一层最多合并两个节点。
区间修改会在覆盖该区间的节点上留下标记,只有在需要继续向下访问时才把标记下传给子节点,因此仍是 O(log n)。
对于求和这类可以用减法撤销的运算,树状数组(Fenwick 树、BIT)只用长度为 n + 1 的数组和几段短循环就能达到相同复杂度。
这是一个迭代版线段树:元素放在长度为 2n 的数组后半部分,前半部分的每个位置保存两个子节点之和。update 从叶节点向上走到根并重新计算和,query 从半开区间 [left, right) 的两端向上走,只累加边界上的节点。运行后依次输出 26、17、28。
segment_tree.py
class SegmentTree:
"""Iterative segment tree: point update and range sum in O(log n)."""
def __init__(self, values):
self.n = len(values)
self.tree = [0] * self.n + list(values)
for i in range(self.n - 1, 0, -1):
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def update(self, index, value):
i = index + self.n
self.tree[i] = value
while i > 1:
i //= 2
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def query(self, left, right):
"""Sum of values[left:right] (half-open range)."""
total = 0
lo, hi = left + self.n, right + self.n
while lo < hi:
if lo & 1:
total += self.tree[lo]
lo += 1
if hi & 1:
hi -= 1
total += self.tree[hi]
lo //= 2
hi //= 2
return total
if __name__ == "__main__":
tree = SegmentTree([5, 3, 7, 9, 6, 4, 1, 2])
print(tree.query(2, 6)) # 7 + 9 + 6 + 4 = 26
tree.update(3, 0)
print(tree.query(2, 6)) # 7 + 0 + 6 + 4 = 17
print(tree.query(0, 8)) # 28
python segment_tree.py共六章,带你从安装一步步了解 线段树 的核心概念。
在这里提问、分享经验,交流关于 线段树 的看法。
还没有讨论。来发起第一个吧。
0 条评论
登录 · 登录后即可发表评论。
来发表第一条评论吧。