Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

线段树 (Segment Tree)

| , 5 minutes reading.

线段树:区间的计算器

算法背后的故事:动态仓库

想象一个有 100 个货架的仓库。每个货架上都有一定数量的箱子。 经理会问两类问题:

  1. 更新: “5 号货架刚多了 3 个箱子。”
  2. 查询: “从 10 号货架到 50 号货架总共有多少个箱子?”
  • 如果你使用普通数组,更新 很快 (O(1)O(1)),但 查询 很慢 (O(N)O(N) - 你必须走过 40 个货架)。
  • 如果你使用预计算的前缀和数组,查询 很快 (O(1)O(1)),但 更新 很慢 (O(N)O(N) - 你必须重新计算 5 号之后的所有和)。

线段树 (Segment Tree) 是中庸之道。它让这两种操作都变得很快 (O(logN)O(\log N))。它将货架分组(1-50, 51-100),并分层(1-25, 26-50),然后记住每个组的总和。

为什么需要它?

  • 计算几何: 寻找重叠的矩形。
  • 分析: “上午 10:00 到下午 2:00 之间的最高股价是多少?”(区间最大值查询)。
  • 游戏开发: “在 X 坐标的这个范围内有墙吗?”

算法是如何“思考”的

这个算法是一个分层聚合器

  1. 叶子: 原始数组的元素是树的叶子。
  2. 内部节点: 每个父节点存储其两个孩子的 总和(或最小值/最大值)。
  3. 查询: 要找到区间 [L,R][L, R] 的总和,我们不查看单独的叶子。我们寻找能放入 [L,R][L, R] 内的最大的预计算“块”,并将它们加起来。
  4. 更新: 当叶子发生变化时,我们向上走到根,只更新受影响的父节点。

工程决策:懒惰传播 (Lazy Propagation)

进阶版本使用 懒惰传播

  • 如果你想“给 1 到 50 号货架的每个货架都加 5 个箱子”,一个个加太慢了。
  • 相反,你在代表 [1-50] 的节点上贴一张“便利贴”(懒标记),上面写着“+5”。直到有人真正查到它的孩子时,你才去更新它们。这使得区间更新也变成了 O(logN)O(\log N)

实现参考 (Python)

class SegmentTree:
    def __init__(self, data):
        self.n = len(data)
        # 树的大小大约是 4*n
        self.tree = [0] * (4 * self.n)
        self._build(data, 1, 0, self.n - 1)

    def _build(self, data, node, start, end):
        if start == end:
            self.tree[node] = data[start]
        else:
            mid = (start + end) // 2
            self._build(data, 2 * node, start, mid)
            self._build(data, 2 * node + 1, mid + 1, end)
            # 示例:求和查询
            self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def update(self, idx, val):
        self._update(1, 0, self.n - 1, idx, val)

    def _update(self, node, start, end, idx, val):
        if start == end:
            self.tree[node] = val
        else:
            mid = (start + end) // 2
            if start <= idx <= mid:
                self._update(2 * node, start, mid, idx, val)
            else:
                self._update(2 * node + 1, mid + 1, end, idx, val)
            self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def query(self, l, r):
        return self._query(1, 0, self.n - 1, l, r)

    def _query(self, node, start, end, l, r):
        if r < start or end < l:
            return 0 # 超出范围
        if l <= start and end <= r:
            return self.tree[node] # 完全在范围内
        
        mid = (start + end) // 2
        p1 = self._query(2 * node, start, mid, l, r)
        p2 = self._query(2 * node + 1, mid + 1, end, l, r)
        return p1 + p2

# 使用示例
data = [1, 3, 5, 7, 9, 11]
st = SegmentTree(data)
print(f"索引 1 到 3 的和: {st.query(1, 3)}") # 3+5+7 = 15
st.update(1, 10) # 把 3 改成 10
print(f"新和: {st.query(1, 3)}") # 10+5+7 = 22

小结

线段树教会我们:聚合能节省时间。通过预先计算群体的摘要,我们无需检查每一个“部分”就能回答关于“整体”的问题。它提醒我们,在数据分析中,缩小视野看到大局的能力与原始数据本身一样重要。