线段树模板:区间加法与区间最小值(Lazy Segment Tree)
线段树模板:区间加法与区间最小值(Lazy Segment Tree)
一、题目描述
给定一个长度为 N 的非负整数序列 A。处理 Q 次查询,查询有以下两种类型:
1 l r x:把区间[l, r]内的所有数加上x2 l r:输出区间[l, r]内的最小值
二、线段树基础知识(带懒标记)
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);
}
逻辑:
- 如果当前区间被查询区间完全覆盖 → 直接更新,并打上懒标记,不再向下递归
- 否则 → 先
pushdown下放标记,再递归左右儿子 - 最后
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 | 完全覆盖则返回,否则下放并递归合并 |

浙公网安备 33010602011771号