你不得不学的线段树 (segment tree) 基础 🌳
你不得不学的线段树基础 🌳
最近刷力扣时遇到了很多和线段树有关的题目,所以这篇博客是用来讲解 线段树 的。
线段树是算法竞赛和面试中常用的数据结构,专门用来高效处理区间信息。它能在 $O(\log N)$ 的时间复杂度内完成区间查询(求和、求最值等)和区间修改操作,将原本 $O(N)$ 的操作大幅优化。
为什么需要线段树?
想象一下,你有一个长度为 $N$ 的数组,你需要频繁地进行以下两种操作:
- 查询操作:求出数组中 $[l, r]$ 区间内所有元素的和(或最大值、最小值等)
- 修改操作:将数组中 $[l, r]$ 区间内的每个元素都加上某个值
如果直接遍历数组:
- 查询操作需要 $O(N)$ 时间
- 修改操作也需要 $O(N)$ 时间
线段树的基本结构
线段树的核心思想是分治:将一个区间不断地二分,直到区间长度为1(叶子节点),每个节点存储其对应区间的某种聚合信息(如和、最大值等)。
存储方式
线段树通常用数组以完全二叉树的形式存储:
- 根节点下标为
1 - 对于节点
p,其左子节点为p*2,右子节点为p*2+1 - 叶子节点对应原始数组的单个元素
让我们通过一个具体例子来理解。假设有数组 $a = [10, 11, 12, 13, 14]$:
1 | |
如何计算节点编号?
- 根节点(管理区间[1,5])编号为1
- 其左子节点(管理区间[1,3])编号为2(1×2)
- 其右子节点(管理区间[4,5])编号为3(1×2+1)
每个节点存储它管理区间的聚合信息(这里是区间和)。
建树过程
建树采用递归二分的方式:
- 如果当前区间长度为1(叶子节点),直接存储该元素的值
- 否则,将区间分为两半:
- 左半部分递归建树
- 右半部分递归建树
- 回溯时,用左右子节点的信息计算当前节点的值
1 | |
空间复杂度分析
线段树需要 $4N$ 的空间来存储:
- 深度为 $\lceil \log N \rceil$
- 总节点数不超过 $2 \times 2^{\lceil \log N \rceil} - 1 \approx 4N$
为什么是4N?
考虑最坏情况:当 $N = 2^x + 1$ 时(如N=9),需要大约 $4N - 5$ 个节点。为保险起见,我们直接分配 $4N$ 的空间。
区间查询
现在我们要查询区间 $[l, r]$ 的和。基本思想是:
- 如果查询区间完全覆盖当前节点区间,直接返回该节点值
- 否则,将查询区间拆分,递归查询左右子树,合并结果
可合并的查询结果
可合并的区间查询指的是查询结果可以通过满足结合律的操作,由两个相邻子区间的结果快速合并得到整个区间的结果,如区间和、区间最值。
1 | |
查询时间复杂度:$O(\log N)$,因为每次递归都将区间长度减半。
区间修改与懒标记
如果每次区间修改都更新所有相关节点,时间复杂度会退化为 $O(N)$。例如,将区间 $[1, 1000]$ 的每个元素都加1,传统方法需要修改1000个叶子节点!
为此引入懒标记(Lazy Propagation)技术,延迟对子节点的更新。
懒标记原理
想象一下,你是一个经理,有一个任务要分配给整个部门。你有两个选择:
- 亲自告诉每个员工(效率低下)
- 告诉部门主管,让他以后找时间通知员工(高效)
懒标记就采用了第二种思路:
- 修改时:只在完全覆盖的节点上更新值并打上标记
- 查询时:如果遇到有标记的节点,先将标记下传给子节点
1 | |
懒标记的时空复杂度
- 时间复杂度:每次更新和查询都是 $O(\log N)$
- 空间复杂度:需要额外的 $4N$ 空间存储懒标记
动态开点线段树
传统线段树需要预先分配 $4N$ 空间,对于区间范围很大(如 $[1, 10^9]$)但操作较少的场景会造成空间浪费。动态开点线段树在需要时才创建节点,节省空间。
核心思想
- 不建立完全二叉树结构
- 只有访问到的节点才会被创建
- 用字典或对象记录左右子节点引用
1 | |
动态开点 vs 静态线段树
| 特性 | 静态线段树 | 动态开点线段树 |
|---|---|---|
| 空间复杂度 | $O(4N)$ | $O(K \log R)$,K为操作次数,R为值域 |
| 适用场景 | 区间范围已知且适中 | 值域很大但操作较少 |
| 实现复杂度 | 简单 | 较复杂 |
| 内存分配 | 一次性分配 | 按需分配 |
总结与反思
线段树是解决区间问题的强大工具,理解其核心思想后,可以扩展到各种变体:
常见线段树变体
- 最大值/最小值线段树:节点存储区间最大/最小值
- 区间赋值线段树:支持将区间设为固定值
- 区间乘加线段树:支持乘法和加法混合操作
- 可持久化线段树(主席树):保存历史版本
- 权值线段树:维护值域统计信息
线段树优化技巧
- 标记永久化:不下传懒标记,在查询时累加路径上的标记
- zkw线段树:非递归实现,常数更小
- 线段树合并:合并两棵线段树的信息
- 线段树分裂:将一棵线段树按值域分裂成两棵