NeFut Logo NeFut
EN 管理员登录

高效区间修改与查询:懒标记线段树的实现与应用

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#C++ #Tutorial

核心逻辑与数学原理

在维护区间修改(如区间加、区间赋值)时,如果采用暴力方法逐一对区间内的每个叶子节点进行单点更新,单次操作的时间复杂度将退化至 $O(N \log N)$。为了保证算法在 $O(\log N)$ 的对数复杂度内完成,引入了懒标记(Lazy Tag)机制。

延迟更新原理

懒标记的核心思想是“只在必要时更新,顺路时下传”。当一个区间修改操作完全覆盖某节点所代表的闭区间 $[L, R]$ 时,更新该节点的数据,并在该节点上打上一个懒标记 $tag$,不再继续向其子树递归。这个标记代表:“该节点的子树数据已经过时,正确的修改增量暂时滞留在当前节点”。

数学正确性维持

假设当前区间 $[L, R]$ 的求和值为 $sum$,被打上了加法懒标记 $v$。

$$sum = sum + (R - L + 1) \times v$$


状态设计与算法推导

1. 状态定义

每个线段树节点除了维护区间和 tree[u] 之外,新增一个标记数组 lazy[u],初始全为 $0$。

2. 标记下传(Pushdown)逻辑

定义 pushdown(u, l, r) 函数。令 $m = (l + r) \gg 1$:

$$lazy[u \ll 1] = lazy[u \ll 1] + lazy[u]$$

$$lazy[u \ll 1 | 1] = lazy[u \ll 1 | 1] + lazy[u]$$

$$tree[u \ll 1] = tree[u \ll 1] + lazy[u] \times (m - l + 1)$$

$$tree[u \ll 1 | 1] = tree[u \ll 1 | 1] + lazy[u] \times (r - m)$$

3. 区间修改(Update)逻辑

定义递归函数 update(u, l, r, ql, qr, val)

  1. 若 $[l, r] \subseteq [ql, qr]$,当前区间被完全包含。直接更新当前节点值,叠加懒标记,终止递归:

$$tree[u] = tree[u] + (r - l + 1) \times val$$

$$lazy[u] = lazy[u] + val$$

  1. 若未被完全包含,说明必须下传标记。首先执行 pushdown(u, l, r)
  2. 令 $m = (l + r) \gg 1$。
  1. 子树递归结束后,调用 pushup(u) 上传最新的正确子节点信息。

C++ 标准源码(NOIP风格)

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

inline long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}

const int MAXN = 100005;
long long tree[MAXN << 2];
long long lazy[MAXN << 2]; // 致命踩坑点:lazy 数组必须与 tree 数组同步开 4 倍空间
long long a[MAXN];

inline void pushup(int u) {
    tree[u] = tree[u << 1] + tree[u << 1 | 1];
}

// 核心下传函数
inline void pushdown(int u, int l, int r) {
    if (lazy[u]) {
        int m = (l + r) >> 1;

        // 下传给左子树
        lazy[u << 1] += lazy[u];
        tree[u << 1] += lazy[u] * (m - l + 1); // 必须乘以左子树的实际区间长度

        // 下传给右子树
        lazy[u << 1 | 1] += lazy[u];
        tree[u << 1 | 1] += lazy[u] * (r - m); // 必须乘以右子树的实际区间长度

        lazy[u] = 0; // 致命踩坑点:下传完毕后必须清空当前节点的物理标记
    }
}

void build(int u, int l, int r) {
    lazy[u] = 0;
    if (l == r) {
        tree[u] = a[l];
        return;
    }
    int m = (l + r) >> 1;
    build(u << 1, l, m);
    build(u << 1 | 1, m + 1, r);
    pushup(u);
}

void update(int u, int l, int r, int ql, int qr, long long val) {
    if (ql <= l && r <= qr) {
        tree[u] += val * (r - l + 1);
        lazy[u] += val; // 完全覆盖时,打上标记即返回,不继续下递归
        return;
    }
    pushdown(u, l, r); // 未完全覆盖,向子树递归前必须先下传旧标记
    int m = (l + r) >> 1;
    if (ql <= m) update(u << 1, l, m, ql, qr, val);
    if (qr > m) update(u << 1 | 1, m + 1, r, ql, qr, val);
    pushup(u); // 子树修改完毕,回溯时用最新状态更新当前节点
}

long long query(int u, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) {
        return tree[u];
    }
    pushdown(u, l, r); // 查询时若要访问子树,也必须下传标记,否则读到过时数据
    int m = (l + r) >> 1;
    long long res = 0;
    if (ql <= m) res += query(u << 1, l, m, ql, qr);
    if (qr > m) res += query(u << 1 | 1, m + 1, r, ql, qr);
    return res;
}

int main() {
    int n = read();
    int m = read();
    for (int i = 1; i <= n; ++i) a[i] = read();

    build(1, 1, n);

    while (m--) {
        int opt = read();
        if (opt == 1) {
            int ql = read(), qr = read();
            long long val = read();
            update(1, 1, n, ql, qr, val);
        } else {
            int ql = read(), qr = read();
            printf("%lld\n", query(1, 1, n, ql, qr));
        }
    }
    return 0;
}

NOIP 实战避坑指南

1. 查询操作忘记调用 pushdown

很多选手误认为只有更新操作(update)需要处理标记。在执行 query 函数时,如果没有在分裂进入子树前执行 pushdown(u, l, r),子树中存储的旧数据将直接被当做答案累加返回,导致逻辑彻底崩溃。记住:只要代码中存在向下递归(即 ql <= mqr > m 分支),前一行必须强制执行 pushdown

2. 标记未清零引发的无限累加

pushdown 逻辑的末尾,必须写上 lazy[u] = 0;。如果遗漏这一行,下一次该节点再次触发 pushdown 时,已经下传过的标记会被重复、错误地叠加到子节点上,引发数据呈线性甚至指数级暴增。

3. 叶子节点触发 pushdown 导致越界

虽然在标准的 updatequery 逻辑中,当 ql <= l && r <= qr 时就已经返回,叶子节点($l = r$)理论上不会进入需要下传标记的分支。但在更复杂的线段树变种中(如动态开点或树上二分),若在 $l = r$ 的情况下强行调用 pushdown,去访问 u << 1 将导致数组下标彻底超越 MAXN << 2 从而触发 RE。在编写防御性代码时,可在 pushdown 开头加入 if (l == r) return;


经典 NOIP/洛谷 真题

1. 洛谷 P3372 【模板】线段树 1

2. 洛谷 P1253 [EGOI2021] 重新排列 / 扶苏的问题

原文链接: local://6.2

[h] 返回首页