线段树学习笔记:区间最大值、单点修改和懒标记

AI 摘要deepseek-v4.1-flash
以7个数的区间最大值为例,从建树、查询、单点修改讲到区间修改与懒标记,逐步展示每个节点在操作中的变化。建树递归合并左右孩子取最大值,查询将区间拆成O(log n)个整块节点,单点修改只更新一条根到叶的路径。懒标记的核心是整段修改时只更新当前节点并记下欠账,需要下探时再通过pushdown交给孩子。结论是线段树通过整块取值和延迟下传实现高效区间操作,并指出数组开4倍、区间和乘长度、下探前pushdown等易错点。
此摘要由 AI 根据正文自动生成,仅供参考

线段树之前一直是半懂不懂的状态,尤其是懒标记:看题解的时候觉得懂了,自己一写就乱。这次干脆拿一个小例子从头推了一遍,把每一步树上发生了什么都画了出来,记在这里。

一句话总结
把区间一直对半劈开,每个节点存「这一段的答案」,父节点的答案由两个孩子拼出来。查一段、改一个数、给一段统一加一个数,都是 O(log n)。

先把树建出来

例子是 7 个数:5 1 4 9 2 7 3,下标从 1 开始,每个节点存这段区间的最大值。规则就一条:区间 [l, r] 从 mid = (l + r) / 2 劈开,左边 [l, mid],右边 [mid + 1, r],劈到只剩一个数为止。

画出来是这样,方括号里是节点编号,后面是区间和这段的最大值:

点节点折叠 / 展开 · 滚轮缩放 · 拖动平移
编号的规律,存数组全靠它
节点 p 的左孩子是 2p,右孩子是 2p+1,父节点是 p/2,所以整棵树直接塞进一个数组,不用指针。
数组要开 4 倍:编号不是连续用满的(上面 7 个数就用到了 13),开 4n 一定够。

建树就是递归下去,叶子直接放元素,回来的时候用两个孩子算自己:

void build(int p, int l, int r) {
    if (l == r) { t[p] = a[l]; return; }          // 叶子:直接存元素
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);                        // 左孩子 2p
    build(p << 1 | 1, mid + 1, r);                // 右孩子 2p+1
    t[p] = max(t[p << 1], t[p << 1 | 1]);         // 合并:取两边最大值
}

查询:能整块拿就整块拿

从根往下走,每到一个节点只有三种情况:

  • 这个节点的区间整段都在查询范围里:直接拿它存的值,不再往下走
  • 和左孩子有交集:去左边
  • 和右孩子有交集:去右边。两边都有就两边都去,最后取 max

比如查 [2, 6] 的最大值,高亮的是最后「整块拿」的节点:

点节点折叠 / 展开 · 滚轮缩放 · 拖动平移

结果是 max(1, 9, 7) = 9。[5] 和 [6] 都是整段拿的,下面的叶子根本没碰。不管查哪一段,最后都只会拆成 O(log n) 个这样的节点,这就是它快的原因。

int query(int p, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) return t[p];          // 当前区间被完全包含
    int mid = (l + r) >> 1, res = INT_MIN;
    if (ql <= mid) res = max(res, query(p << 1, l, mid, ql, qr));
    if (qr > mid)  res = max(res, query(p << 1 | 1, mid + 1, r, ql, qr));
    return res;
}

单点修改:改叶子,回来的路上顺手更新

把第 4 个数从 9 改成 0。从根往下只走包含位置 4 的那一边:[1] → [2] → [5] → [11],把 [11] 改掉,然后回溯的时候一路往上重新算 max:

点节点折叠 / 展开 · 滚轮缩放 · 拖动平移

原来的最大值 9 没了,根节点跟着变成 7。整个过程只动了一条从根到叶子的路径。

区间修改和懒标记(最绕的一块)

现在要给一整段加上同一个数。拆成一个一个单点改的话,一次就要 O(n log n),比暴力还慢。

懒标记的想法其实就是偷懒:走到一个整段都要加的节点,只改它自己的值,然后在它身上记一笔「我下面的都还欠着 +v」,不往下走。等以后真的要往它下面走的时候,再把这笔账交给两个孩子,这一步叫 pushdown。

接着上面的数组(第 4 个数已经是 0 了)做两次区间加:

① [5, 7] 全部 +3:走到 [3](5~7)正好整段都在,[3] 的值 7 → 10,记上 tag = 3,到此为止。[6]、[7] 和下面的叶子都还是旧值。

② [5, 6] 全部 +2:又走到 [3],这次不是整段,得往下走了,走之前先把 [3] 欠的 +3 交下去:[6] 7 → 10,[7] 3 → 6,它俩各记上 tag 3,[3] 的 tag 清零。然后来到 [6](5~6),整段都在:10 → 12,tag 变成 3 + 2 = 5。

两次做完,树上是这样的(标着「旧值」的两个叶子,其实还欠着 [6] 身上的 +5):

点节点折叠 / 展开 · 滚轮缩放 · 拖动平移

③ 查 [6, 6]:走到 [6] 要继续往下,发现它身上有 tag 5,先交下去:[12] 2 → 7,[13] 7 → 12,然后到 [13] 拿到 12。核对一下:第 6 个数是 7,先 +3 再 +2,正好 12。

有了懒标记,就两条规矩
往下走之前,先 pushdown:查询、修改都一样
递归回来之后,再 pushup:用两个孩子重新算自己

pushdown 写出来就这几行(这里是区间和的版本,配合下面的 P3372):

// 给节点 p(区间长度 len)整体加上 v:更新自己的值,并把这次修改记在懒标记上
void addTag(int p, int len, ll v) {
    sum[p] += v * len;     // 区间和:每个元素都 +v,总和 +v*len
    tag[p] += v;
}

// 下发懒标记:把欠着孩子的修改交给左右孩子,自己清零
void pushdown(int p, int l, int r) {
    if (!tag[p]) return;
    int mid = (l + r) >> 1;
    addTag(p << 1, mid - l + 1, tag[p]);
    addTag(p << 1 | 1, r - mid, tag[p]);
    tag[p] = 0;
}

做了两道洛谷题

P1531 I Hate It

单点修改 + 区间最大值,就是前面那一套。U A B 是「A 的成绩比 B 低才改成 B」,所以叶子上是取 max,不是直接赋值。

P3372 【模板】线段树 1

区间加 + 区间求和,懒标记的模板题。和最大值版本只差一个地方:

最容易错的一处
维护区间和时,一个节点整体 +v,它的值要加 v × 区间长度;维护最大值才是只加 v。
另外和会很大,记得开 long long。

两份代码都过了样例,也和暴力对拍过。

容易写错的地方

  • 数组没开 4 倍
  • 区间和忘了乘区间长度
  • 往下走之前忘了 pushdown,回来之后忘了 pushup
  • 区间和没开 long long

接下来打算做 P3373 【模板】线段树 2(加和乘两个标记,要想清楚先乘后加)和 P1253 扶苏的问题(区间赋值 + 区间加 + 区间最大值)。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇