线段树模板:区间加法与区间最小值(Lazy Segment Tree)

线段树模板:区间加法与区间最小值(Lazy Segment Tree)

一、题目描述

给定一个长度为 N 的非负整数序列 A。处理 Q 次查询,查询有以下两种类型:

  • 1 l r x:把区间 [l, r] 内的所有数加上 x
  • 2 l r:输出区间 [l, r] 内的最小值

对应题目:AtCoder Lazy Segment Tree (B - Lazy Segment Tree)

二、线段树基础知识(带懒标记)

2.1 为什么需要懒标记(Lazy Tag)

如果每次区间修改都暴力遍历 [l, r] 中的每个元素,单次修改的复杂度是 O(n),总体复杂度 O(nq),显然会超时。

懒标记的核心思想:如果当前区间被完全包含在修改范围内,就不再向下递归,而是打上一个标记(懒标记),表示“这个区间内的所有数都要加上 x”,并且把当前区间的答案一并更新。只有当后续操作需要访问子区间时,才把标记下发给左右儿子。

2.2 节点信息

struct node
{
    int L, R;
    ll v;      // 当前区间的最小值
    ll lazy;   // 懒标记:表示该区间所有数都要加上 lazy
} seg[N * 4];
  • v:区间最小值
  • lazy:区间加法懒标记(初始化为 0,表示没有修改)

三、核心操作详解

3.1 push_up(向上更新)

void push_up(int p)
{
    seg[p].v = min(seg[p << 1].v, seg[p << 1 | 1].v);
}

父节点的最小值 = 左右儿子最小值的较小者。

3.2 pushdown(向下下放懒标记)

void pushdown(int p)
{
    if (seg[p].lazy == 0) return;
    int lc = p << 1, rc = p << 1 | 1;

    // 更新左儿子
    seg[lc].v += seg[p].lazy;
    seg[lc].lazy += seg[p].lazy;

    // 更新右儿子
    seg[rc].v += seg[p].lazy;
    seg[rc].lazy += seg[p].lazy;

    // 清空当前节点的标记
    seg[p].lazy = 0;
}

注意:加法懒标记可以被累加。如果已经有标记,新的标记直接加上去即可(seg[lc].lazy += seg[p].lazy)。

3.3 update(区间修改)

void update(int p, int L, int R, ll v)
{
    int l = seg[p].L, r = seg[p].R;
    if (l >= L && r <= R) 
    {
        seg[p].v += v;
        seg[p].lazy += v;
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (L <= mid) update(p << 1, L, R, v);
    if (R > mid) update(p << 1 | 1, L, R, v);
    push_up(p);
}

逻辑:

  1. 如果当前区间被查询区间完全覆盖 → 直接更新,并打上懒标记,不再向下递归
  2. 否则 → 先 pushdown 下放标记,再递归左右儿子
  3. 最后 push_up 更新当前节点

3.4 query(区间查询)

ll query(int p, int L, int R)
{
    int l = seg[p].L, r = seg[p].R;
    if (l >= L && r <= R) return seg[p].v;
    pushdown(p);
    int mid = (l + r) >> 1;
    ll res = INF;
    if (L <= mid) res = min(res, query(p << 1, L, R));
    if (R > mid) res = min(res, query(p << 1 | 1, L, R));
    return res;
}

查询时,无论是否完全覆盖,必须先 pushdown,否则子区间的值可能是陈旧的。

四、AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N = 2e5 + 5;
const ll INF = 1e18;

ll arr[N];

struct node
{
    int L, R;
    ll v;
    ll lazy;
} seg[N * 4];

void push_up(int p)
{
    seg[p].v = min(seg[p << 1].v, seg[p << 1 | 1].v);
}

void build(int p, int L, int R)
{
    seg[p].L = L, seg[p].R = R;
    seg[p].lazy = 0;
    if (L == R)
    {
        seg[p].v = arr[L];
        return;
    }
    int mid = (L + R) >> 1;
    build(p << 1, L, mid);
    build(p << 1 | 1, mid + 1, R);
    push_up(p);
}

void pushdown(int p)
{
    if (seg[p].lazy == 0) return;
    int lc = p << 1, rc = p << 1 | 1;

    seg[lc].v += seg[p].lazy;
    seg[lc].lazy += seg[p].lazy;

    seg[rc].v += seg[p].lazy;
    seg[rc].lazy += seg[p].lazy;

    seg[p].lazy = 0;
}

ll query(int p, int L, int R)
{
    int l = seg[p].L, r = seg[p].R;
    if (l >= L && r <= R) return seg[p].v;

    pushdown(p);
    int mid = (l + r) >> 1;
    ll res = INF;

    if (L <= mid) res = min(res, query(p << 1, L, R));
    if (R > mid) res = min(res, query(p << 1 | 1, L, R));

    return res;
}

void update(int p, int L, int R, ll v)
{
    int l = seg[p].L, r = seg[p].R;
    if (l >= L && r <= R)
    {
        seg[p].v += v;
        seg[p].lazy += v;
        return;
    }

    pushdown(p);
    int mid = (l + r) >> 1;
    if (L <= mid) update(p << 1, L, R, v);
    if (R > mid) update(p << 1 | 1, L, R, v);

    push_up(p);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> arr[i];
    build(1, 1, n);
    while (q--)
    {
        int op;
        cin >> op;
        if (op == 1)
        {
            int L, R;
            ll x;
            cin >> L >> R >> x;
            update(1, L, R, x);
        }
        else
        {
            int L, R;
            cin >> L >> R;
            cout << query(1, L, R) << "\n";
        }
    }

    return 0;
}

五、复杂度分析

操作 时间复杂度
建树 O(n)
区间修改 O(log n)
区间查询 O(log n)
空间复杂度 O(n)

带懒标记的线段树能够高效处理区间修改与查询,是解决此类问题的标准工具。

六、易错点提醒

  • lazy 初始化为 0(如果是加法标记),表示没有修改;如果是赋值标记,通常初始化为 -1。
  • update 时先更新当前节点,再判断是否要递归。不要漏掉 lazy 的累加。
  • query 和 update 在递归前必须先 pushdown,否则子区间的值不正确。
  • pushdown 时不要忘记清空父节点的 lazy,否则会重复下放。
  • 数据范围到 10^9,累加会超过 int,必须用 long long。
  • 如果题目有多个测试用例,需要重置 lazy 数组。

七、总结

模块 核心要点
节点结构 维护 v(答案)和 lazy(标记)
push_up 更新父节点答案
pushdown 下放标记,更新子节点答案
update 完全覆盖则打标记,否则下放并递归
query 完全覆盖则返回,否则下放并递归合并
posted @ 2026-10-27 11:35  leaderling  阅读(9)  评论(0)    收藏  举报