如果你曾因为一道需要对数组反复更新并查询区间数千次的题目而超时,那么线段树正是把这堵墙变成简洁、对数级解法的数据结构。
线段树是竞赛编程中最重要的工具之一。它能回答关于数组某段连续区间的问题,例如求和、最小值、最大值或最大公约数,同时还能修改数组,每次操作都只需 O(log n) 的时间。当一道题混合了大量更新与大量查询时,正是这种效率把能通过的解法与会超时的解法区分开来。
朴素数组为何力不从心
设想一个有一百万个数字的数组。用循环回答单次「从下标 L 到 R 求和」的问题是 O(n),而单次单点更新是 O(1)。这听起来还不错,直到题目抛出几十万次混合的更新与查询。总工作量会膨胀到每次查询大约 O(n),这就太慢了。
前缀和数组可以解决区间求和查询,但一旦数值发生改变就会失效,因为每次更新都迫使你重建前缀和。线段树则一次性解决了问题的两个方面:快速查询与快速更新,即便它们交错出现也不在话下。
经验法则:当一道题把区间查询与更新结合起来,且数组规模很大时,就该考虑线段树(对于更简单的纯求和场景则可用树状数组)。
线段树的工作原理
线段树是一棵基于分治思想构建的二叉树。根节点代表整个数组。每个内部节点代表一段区间,并把该区间一分为二,分别存储在它的两个子节点中。叶子节点代表单个元素。
每个节点存储其区间的某种聚合值,例如和或最小值。由于这棵树大约有 2n 个节点,高度约为 log n,两项核心操作便变得开销很小:
- 区间查询:从根节点向下遍历,合并那些完全落在查询区间内的节点的预计算值,只对部分重叠的节点进行递归。最多触及 O(log n) 个节点。
- 单点更新:改变一个叶子节点,然后向上移动并重新计算受影响的祖先节点。只有从叶子到根的一条路径会变化,因此是 O(log n)。
「合并」这一步正是结构变得灵活的地方。把加法换成 min、max 或 gcd,同样的骨架就能回答一个完全不同的问题。练就这种思维上的灵活替换,正是我们在 BIAA 竞赛编程方向中反复训练的技能之一。
用于区间更新的懒标记下传
对于许多难题,单点更新还不够。有些题目要求你给某个区间内的每一个元素加上一个值,或者把一个值赋给整段区间。逐个更新每个叶子又会回到 O(n)。
解决办法是懒标记下传。你不必立刻把更新一路推到底,而是把一个待处理的改动(称为懒标记)存储在完全覆盖更新区间的最高层节点上。这项工作被推迟,直到后续某次查询或更新确实需要穿过该节点时,你才把标记「下推」到它的子节点。
初学者最容易忘记的一条规则:在递归进入某个节点的子节点之前,务必先下推待处理的标记。一旦遗漏,更新就会悄无声息地丢失,产生几乎正确却几乎无法调试的答案。
有了懒标记下传,区间更新和区间查询都能以 O(log n) 运行。这一模式在较难的 USACO Gold 和 Platinum 题目以及整个 Codeforces 中不断出现,因此非常值得精通。
一份练习路线图
- 构建一棵支持单点更新和区间求和查询的求和线段树。
- 把它改写为支持区间最小值和区间最大值查询,以吃透合并这一步。
- 加入懒标记下传以支持区间加法,再支持区间赋值。
- 在基础操作变得驾轻就熟之后,再攻克二维区间查询和 segment-tree-beats 等变体。
线段树更看重深思熟虑、有条理的练习,而非单纯的天赋。备战 USACO 比赛的学生会在通往 Gold 和 Platinum 的路上很早就遇到这一结构,而一份胸有成竹的实现能在赛场上省下宝贵的几分钟。
如果你想要有指导的练习、代码评审,以及从数组到高级数据结构的清晰进阶路径,欢迎了解 BIAA 竞赛编程项目,开始打造顶尖选手所依赖的工具箱。