GESP 8级

线段树

8级 · 高级数据结构与DP
线段树(Segment Tree)是一种支持区间查询和区间修改的数据结构。相比树状数组,线段树功能更强大——可以处理区间最值、区间求和、区间修改等,是竞赛中最强的"万能工具"之一。
💡 这是什么?
生活比喻:想象你是一栋教学楼的管理员,需要知道"第3楼到第7楼一共有多少学生"。
• 笨办法:一间一间教室去数 → O(n),太慢
• 线段树:每层楼提前统计好总人数,查询时只需合并几个"段"

核心思想:把数组想象成一条线段,递归地把线段一分为二,每个"节点"存储对应区间的答案(和、最值等)。

树状数组 vs 线段树:
• 树状数组:代码短,能做"单点修改+区间查询",但不能做区间修改
• 线段树:代码长一点,能做"区间修改+区间查询",功能更全面
• 考试中如果需要区间修改(比如"把第l到第r个元素都加上x"),必须用线段树
🌟 为什么重要?
• 区间操作的王者:区间最值、区间求和、区间修改,线段树都能做
• 懒惰标记(lazy propagation):线段树的经典技巧,实现区间修改的关键
• 竞赛必备:信息学竞赛中出现频率极高的数据结构
• 可以扩展:主席树、线段树合并、二维线段树等高级应用
• GESP 8级重点:比树状数组更常考
📋 前置知识(学这个之前你需要知道)
1. 递归(GESP 3-4级):线段树的 build、update、query 都是递归函数
2. 二叉树概念(GESP 5级):线段树本质上是一棵二叉树
3. 区间/前缀和(GESP 2-3级):理解"区间的和"是什么意思
4. 可选:树状数组:先学树状数组再学线段树会更容易理解
📐 线段树结构(以区间求和为例)
数组 a[1..8] = [3, 1, 2, 4, 5, 3, 1, 6]

线段树的每个节点表示一个区间 [l, r]:
根节点 [1,8] = 所有元素的和 = 25
左子 [1,4] = 3+1+2+4 = 10
右子 [5,8] = 5+3+1+6 = 15
... 递归分下去,直到叶子节点 [i,i] = a[i]

树的大小:开 4*n 的数组(比 n 大很多但够用)
💻 完整代码 — 区间求和 + 区间修改(带 lazy 懒惰标记)
#include <iostream>
using namespace std;

const int N = 100010;

int a[N];       // 原始数组
long long tree[4*N];  // 线段树:每个节点存对应区间的和
long long lazy[4*N];  // 懒惰标记:延迟下传的修改量
int n;          // 数组大小

// pushUp:用左右子节点的值更新当前节点
// 当前节点的值 = 左子 + 右子
void pushUp(int node) {
    tree[node] = tree[node*2] + tree[node*2+1];
}

// pushDown:把懒惰标记下传给左右子节点
// 当某个节点有 lazy 值没下传时调用
void pushDown(int node, int l, int r) {
    if (lazy[node] == 0) return;  // 没有需要下传的修改
    int mid = (l + r) / 2;
    // 左子区间 [l, mid],长度 = mid-l+1
    tree[node*2]   += lazy[node] * (mid - l + 1);
    lazy[node*2]   += lazy[node];
    // 右子区间 [mid+1, r],长度 = r-mid
    tree[node*2+1] += lazy[node] * (r - mid);
    lazy[node*2+1] += lazy[node];
    // 当前节点的标记已下传,清零
    lazy[node] = 0;
}

// 建树:递归构建线段树
// node=当前节点编号, [l,r]=当前节点管辖的区间
void build(int node, int l, int r) {
    if (l == r) {
        // 叶子节点:直接等于原始数组的值
        tree[node] = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(node*2,   l,   mid);  // 递归建左子树
    build(node*2+1, mid+1, r);  // 递归建右子树
    pushUp(node);               // 用子节点更新当前节点
}

// 区间修改:把 [ql, qr] 内的所有元素加上 val
void update(int node, int l, int r, int ql, int qr, long long val) {
    if (ql <= l && r <= qr) {
        // 当前区间完全被查询区间覆盖
        tree[node] += val * (r - l + 1);  // 区间和增加 val×长度
        lazy[node] += val;                // 记录懒惰标记,暂不下传
        return;
    }
    // 部分覆盖:需要递归到子节点
    pushDown(node, l, r);  // 先下传懒惰标记
    int mid = (l + r) / 2;
    if (ql <= mid) update(node*2,   l,   mid, ql, qr, val);
    if (qr > mid)  update(node*2+1, mid+1, r, ql, qr, val);
    pushUp(node);  // 子节点更新后,更新当前节点
}

// 区间查询:求 [ql, qr] 的和
long long query(int node, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) {
        return tree[node];  // 完全覆盖,直接返回
    }
    pushDown(node, l, r);  // 先下传懒惰标记
    int mid = (l + r) / 2;
    long long res = 0;
    if (ql <= mid) res += query(node*2,   l,   mid, ql, qr);
    if (qr > mid)  res += query(node*2+1, mid+1, r, ql, qr);
    return res;
}

int main() {
    int q;  // 操作次数
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];

    build(1, 1, n);  // 建树,根节点编号为1

    while (q--) {
        int op;
        cin >> op;
        if (op == 1) {
            // 区间修改:把 l 到 r 的每个元素加上 x
            int l, r; long long x;
            cin >> l >> r >> x;
            update(1, 1, n, l, r, x);
        } else {
            // 区间查询:求 l 到 r 的和
            int l, r;
            cin >> l >> r;
            cout << query(1, 1, n, l, r) << endl;
        }
    }
    return 0;
}
🔍 懒惰标记(Lazy Propagation)原理
问题:如果要"把 [3,7] 每个数+2",朴素做法要递归到5个叶子节点,太慢。

懒惰标记的思路:
① 当修改区间完全覆盖某个节点的区间时,不继续递归
② 只更新当前节点的 tree 值,同时在 lazy 数组中记录"这个区间还欠着一个+2的修改没下传"
③ 以后如果需要访问这个节点的子节点,先"下传"(pushDown)——把修改传递给子节点

这就像老师说"全班每人加2分",但不用真的一个个改卷子,先在成绩单上记一笔"全班+2",等到需要排名时再逐个加。

关键:pushDown 在 update 和 query 中都要先调用!
⏱ 复杂度分析
建树:O(n),递归构建每个节点
区间修改(lazy):O(log n),最多走到树的高度
区间查询:O(log n),最多走到树的高度
空间:O(4n),数组要开 4 倍大小

对比:普通数组区间修改 O(n),区间查询 O(1) 或 O(n)
线段树两种操作都是 O(log n),在大量操作时优势巨大
⚠️ 易错点
1. 数组开4*n! → 线段树最多有 4n 个节点,开小了会越界。
2. pushDown 不能忘 → update 和 query 进入子节点前必须先 pushDown,否则懒惰标记没下传,数据会错。
3. lazy 用 += 不用 = → 多次修改时,lazy 要累加,不能覆盖。
4. lazy 的值要乘区间长度 → 区间每个数+val,总和要加 val×(r-l+1)。
5. node*2 和 node*2+1 的区间 → 左子 [l, mid],右子 [mid+1, r],别写反。
6. 边界条件 → 叶子节点时 l==r,不要递归到 l>r 的情况。
🎯 练习建议
入门练习:
• 洛谷 P3372 【模板】线段树 1(区间修改 + 区间查询,最基础的模板)
• 洛谷 P3373 【模板】线段树 2(区间修改 + 区间查询,带取模)

进阶练习:
• 洛谷 P2801 教主的魔法(区间修改 + 二分查找)
• 洛谷 P1908 逆序对(可以用线段树做,对比树状数组)

学习方法:
① 先学"无 lazy 的线段树"(只有单点修改+区间查询),理解递归结构
② 再加入 lazy 机制,理解 pushDown 的必要性
③ 画一棵小的线段树(比如 n=8),手动模拟 build、update、query
④ 代码较长,建议背模板,考试时直接套用