本课时有配套视频讲解,购买后即可观看(永久有效)
线段树2
一、课上练习
编程练习
- 线段树2:L49968
二、知识总结
✨ 核心思想
当线段树需要支持区间修改(如将 [l, r] 的每个元素加上一个值)时,逐个修改叶子节点的复杂度退化为 O(n)。
懒标记(Lazy Propagation)的核心思想是:修改操作先打标记、后下传。当修改的区间完全覆盖某个节点时,只在该节点上记录标记,不立即递归到子节点。只有当需要访问子节点时,才将标记下传(pushDown),从而保证每次操作只影响 O(log n) 个节点。
✨ 算法原理
懒标记的三个核心操作
- 打标记(addTag):给节点打上懒标记,同时更新节点自身的值。
- 下传标记(pushDown):将当前节点的懒标记传递给两个子节点,然后清除自身标记。
- 回溯更新(pushUp):与基础线段树相同,用子节点更新父节点。
区间修改流程
- 如果当前节点区间被查询区间完全包含 → 打标记并返回
- 否则先 pushDown 下传标记,再递归左右子节点
- 回溯时 pushUp
区间查询流程
- 如果当前节点区间被查询区间完全包含 → 直接返回
- 否则先 pushDown 下传标记,再递归查询
- 合并左右子节点的查询结果
✨ 代码实现
线段树 - 懒标记实现区间加 + 区间求和
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAXN = 200005;
5typedef long long ll;
6
7int a[MAXN];
8int n, m;
9
10struct SegTree {
11 ll sum[MAXN * 4]; // 区间和
12 ll lazy[MAXN * 4]; // 懒标记:区间加的值
13 int sz[MAXN * 4]; // 区间长度(用于pushDown时计算)
14
15 void pushUp(int p) {
16 sum[p] = sum[p * 2] + sum[p * 2 + 1];
17 }
18
19 // 给节点p打标记:区间内每个元素加val
20 void addTag(int p, ll val) {
21 lazy[p] += val;
22 sum[p] += val * sz[p]; // 区间和增加 val * 区间长度
23 }
24
25 // 下传懒标记
26 void pushDown(int p) {
27 if (lazy[p] != 0) {
28 addTag(p * 2, lazy[p]);
29 addTag(p * 2 + 1, lazy[p]);
30 lazy[p] = 0; // 清除当前节点的标记
31 }
32 }
33
34 void build(int p, int l, int r) {
35 sz[p] = r - l + 1;
36 lazy[p] = 0;
37 if (l == r) {
38 sum[p] = a[l];
39 return;
40 }
41 int mid = (l + r) / 2;
42 build(p * 2, l, mid);
43 build(p * 2 + 1, mid + 1, r);
44 pushUp(p);
45 }
46
47 // 区间修改:[ql, qr]每个元素加val
48 void update(int p, int l, int r, int ql, int qr, ll val) {
49 if (ql <= l && r <= qr) {
50 addTag(p, val); // 完全包含,打标记
51 return;
52 }
53 pushDown(p); // 先下传标记
54 int mid = (l + r) / 2;
55 if (ql <= mid) update(p * 2, l, mid, ql, qr, val);
56 if (qr > mid) update(p * 2 + 1, mid + 1, r, ql, qr, val);
57 pushUp(p);
58 }
59
60 // 区间查询:求[ql, qr]的和
61 ll query(int p, int l, int r, int ql, int qr) {
62 if (ql <= l && r <= qr) return sum[p];
63 pushDown(p); // 先下传标记
64 int mid = (l + r) / 2;
65 ll res = 0;
66 if (ql <= mid) res += query(p * 2, l, mid, ql, qr);
67 if (qr > mid) res += query(p * 2 + 1, mid + 1, r, ql, qr);
68 return res;
69 }
70} seg;
71
72int main() {
73 ios::sync_with_stdio(false);
74 cin.tie(nullptr);
75
76 cin >> n >> m;
77 for (int i = 1; i <= n; i++) cin >> a[i];
78
79 seg.build(1, 1, n);
80
81 while (m--) {
82 int op;
83 cin >> op;
84 if (op == 1) {
85 int l, r;
86 ll val;
87 cin >> l >> r >> val;
88 seg.update(1, 1, n, l, r, val);
89 } else {
90 int l, r;
91 cin >> l >> r;
92 cout << seg.query(1, 1, n, l, r) << "\n";
93 }
94 }
95
96 return 0;
97}✨ 执行示例
以数组 a = [1, 2, 3, 4, 5](n=5)为例:
初始线段树:
| 节点 | 区间 | sum | lazy | sz |
|---|---|---|---|---|
| 1 | [1,5] | 15 | 0 | 5 |
| 2 | [1,3] | 6 | 0 | 3 |
| 3 | [4,5] | 9 | 0 | 2 |
| 4 | [1,2] | 3 | 0 | 2 |
| 5 | [3,3] | 3 | 0 | 1 |
| 6 | [4,4] | 4 | 0 | 1 |
| 7 | [5,5] | 5 | 0 | 1 |
操作 update(1, 1, 5, 2, 4, 3) — 将 a[2]~a[4] 每个加 3:
- 节点[1,5]:[2,4] 部分重叠 → pushDown(无标记) → 递归
- 节点[1,3]:[2,4] 部分重叠 → pushDown → 递归
- 节点[1,2]:[2,4] 部分重叠 → pushDown → 递归右子
- 节点[2,2]:[2,4] 完全包含 → addTag(+3) → sum=5
- 节点[1,2] pushUp:sum = 1+5 = 6
- 节点[3,3]:[2,4] 完全包含 → addTag(+3) → sum=6
- 节点[1,3] pushUp:sum = 6+6 = 12
- 节点[4,5]:[2,4] 部分重叠 → pushDown → 递归左子
- 节点[4,4]:[2,4] 完全包含 → addTag(+3) → sum=7
- 节点[4,5] pushUp:sum = 7+5 = 12
- 节点[1,5] pushUp:sum = 12+12 = 24
验证:(1+2+3+4+5) + 3×3 = 15+9 = 24 ✓
查询 query(1, 1, 5, 1, 3):
- 节点[1,3] 被 [1,3] 完全包含 → 返回 sum=12
- 结果 = 12,即 1 + (2+3) + (3+3) = 12 ✓
✨ 解题步骤详解
设计懒标记的关键问题
- 标记代表什么操作? 区间加法标记
lazy表示"该区间内每个元素都要加上 lazy 的值"。 - 打标记时怎么更新节点值? 区间和增加
val × sz(区间长度)。 - 标记怎么合并? 两次加法标记可以直接累加:
lazy += val。 - 下传时怎么处理? 将父节点标记分别传给两个子节点,然后清零。
懒标记的正确性保证
- 不变量:任何时刻,从根到叶子路径上所有懒标记的累加,加上叶子节点当前值,等于该元素的真实值。
- pushDown 操作确保在需要精确值时,所有祖先标记已经下传。
✨ 常见错误
- 查询时忘记 pushDown:必须在递归子节点之前下传标记,否则子节点信息不是最新的。
- pushDown 后忘记清零标记:标记已传给子节点,当前节点必须清零。
- addTag 中忘记乘以区间长度:
sum[p] += val * sz[p],不是sum[p] += val。 - 建树时忘记初始化 lazy 和 sz:lazy 必须初始化为 0,sz 必须正确设置。
- 修改时先 pushUp 再 pushDown:顺序是先 pushDown 再递归再 pushUp。
✨ 算法评价
| 指标 | 复杂度 |
|---|---|
| 建树时间 | O(n) |
| 区间修改 | O(log n) |
| 区间查询 | O(log n) |
| 空间复杂度 | O(n) |
懒标记的意义:
- 将区间修改从 O(n) 优化到 O(log n)
- 是线段树最重要的技巧,几乎所有进阶线段树题目都需要
对比树状数组:
- 线段树+懒标记功能更强,但常数更大
- 如果只需要区间加+区间求和,树状数组更快更简洁