线段树的区间(加、乘)修改、区间询问
线段树的区间(加、乘)修改、区间询问
学习是一个持续的过程,每一小步都是进步。
———— 我也不知道是谁说的
1. 分析问题:修改、询问
- 痛点:直接暴力修改区间每个元素,时间复杂度会退化为 O(n) ,效率极低。
- 优化:懒标记(lazy tag) ,当修改区间完全覆盖线段树中某个节点区间时,不立即更新所有子节点,而是记录修改操作(打标记),等后续查询/修改需要访问子节点时,再下传标记,保证时间复杂度仍为 O(logn) 。
- 结构扩展:在节点结构体中加 add 字段,记录待下传的区间修改值(如区间每个元素加 add )。
核心操作: - pushdown():下传懒标记,更新子节点的统计值和子节点的懒标记。
- update_range:区间修改主逻辑,覆盖时打标记,否则分裂区间,下传标记后递归修改。
- 例如,在 [1,10] 的线段树中修改 [4,8] 区间的每个值增加 20,找到完全覆盖的节点(如 [4,5]、[6,8] 等 ),打标记记录 +20,不立即更新子节点;后续查询/修改涉及这些子节点时,通过 pushdown() 下传标记,更新子节点值和标记。
2. 区间修改的关键:lazy-tag
lazy: 修改一个整块区间时,只对这个线段区间进行整体上的修改,其内部每个元素的内容先不做修改。只有当这部分线段的一致性被破坏时,才把变化值传递给子区间。
tag: 对做 lazy 操作的子区间,记录状态
3. 区间加法
- 下传懒标记:将当前节点的 add 标记传递给左右子节点
void pushdown(int p) {
if (tr[p].add) { // 有懒标记需要下传
// 更新左孩子
tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
tr[lc].add += tr[p].add;
// 更新右孩子
tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
tr[rc].add += tr[p].add;
// 清除当前节点的懒标记
tr[p].add = 0;
}
}
- 区间更新:给区间 [x,y] 每个数加 k
void update_interval(int p, int x, int y, ll k) {
// 完全覆盖,直接更新sum和懒标记
if (x <= tr[p].l && tr[p].r <= y) {
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].add += k;
return ;
}
pushdown(p); // 递归子节点前先下传懒标记
int m = (tr[p].l + tr[p].r) >> 1;
if (x <= m) update_interval(lc, x, y, k); // 左子树有重叠
if (y > m) update_interval(rc, x, y, k); // 右子树有重叠
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新当前节点sum
}
- 完整代码
#include <bits/stdc++.h>
using namespace std;
#define N 100010
#define ll long long
#define lc p << 1 // 左孩子编号:p*2
#define rc p << 1|1 // 右孩子编号:p*2+1
int n, m;
ll w[N]; // 原数组,用ll防止溢出
struct tree {
ll l, r, sum, add; // add用于懒标记(区间更新专属优化)
};
tree tr[N * 4];
// 下传懒标记:将当前节点的add标记传递给左右子节点
void pushdown(int p) {
if (tr[p].add) { // 有懒标记需要下传
// 更新左孩子
tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
tr[lc].add += tr[p].add;
// 更新右孩子
tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
tr[rc].add += tr[p].add;
// 清除当前节点的懒标记
tr[p].add = 0;
}
}
void build(int p, int l, int r) { // p:根节点
tr[p] = {l, r, w[l], 0}; // add先置0
if(l == r) return ;
int m = (l + r) >> 1;
build(lc, l, m); // 左子树
build(rc, m + 1, r); // 右子树
tr[p].sum = tr[lc].sum + tr[rc].sum; // 当前节点sum=左右子树sum之和
}
// 区间更新:给区间[x,y]每个数加k
void update_interval(int p, int x, int y, ll k) {
// 完全覆盖,直接更新sum和懒标记
if (x <= tr[p].l && tr[p].r <= y) {
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].add += k;
return ;
}
pushdown(p); // 递归子节点前先下传懒标记
int m = (tr[p].l + tr[p].r) >> 1;
if (x <= m) update_interval(lc, x, y, k); // 左子树有重叠
if (y > m) update_interval(rc, x, y, k); // 右子树有重叠
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新当前节点sum
}
// 单点更新:给第pos个位置的数加k(修复:添加pushdown调用)
void update_single(int p, int pos, ll k) {
// 找到目标叶子节点
if (tr[p].l == tr[p].r) {
tr[p].sum += k; // 更新叶子节点的sum(单点的和就是自身值)
return;
}
pushdown(p);
int m = (tr[p].l + tr[p].r) >> 1;
// 判断目标位置在左子树还是右子树,递归更新对应子树
if (pos <= m) update_single(lc, pos, k); // 左子树包含目标位置
else update_single(rc, pos, k); // 右子树包含目标位置
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新当前节点sum
}
// 区间查询(查询区间[x,y]的和)
ll query(int p, int x, int y) {
if (x <= tr[p].l && tr[p].r <= y) { // 完全覆盖,直接返回
return tr[p].sum;
}
pushdown(p); // 递归子节点前先下传懒标记
int m = (tr[p].l + tr[p].r) >> 1;
ll sum = 0;
if (x <= m) sum += query(lc, x, y); // 左子树有重叠
if (y > m) sum += query(rc, x, y); // 右子树有重叠
return sum;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i){
cin >> w[i];
}
build(1, 1, n); // 构建线段树,根节点为1,覆盖[1,n]
for(int i = 1; i <= m; ++i){
int zwy;
cin >> zwy;
if(zwy == 1){
int x, y;
ll k;
cin >> x >> y >> k;
update_interval(1, x, y, k); // 区间更新(指令1)
}
else if(zwy == 2){
int pos;
ll k;
cin >> pos >> k;
update_single(1, pos, k); // 单点更新(指令2)
}
else{
int x, y;
cin >> x >> y;
cout << query(1, x, y) << '\n'; // 区间查询(指令3)
}
}
return 0;
}
4. 区间加、乘法
- 下传懒标记:将当前节点的 mul 和 add 标记传递给左右子节点
void pushdown(int p) {
// 无孩子
if (tr[p].l == tr[p].r) return;
// 乘法懒标记
if (tr[p].mul != 1) {
// 更新左孩子
tr[lc].sum *= tr[p].mul;
tr[lc].mul *= tr[p].mul; // 乘法标记下传
tr[lc].add *= tr[p].mul; // 加法标记乘以mul
// 更新右孩子
tr[rc].sum *= tr[p].mul;
tr[rc].mul *= tr[p].mul;
tr[rc].add *= tr[p].mul;
// 清除乘法懒标记
tr[p].mul = 1;
}
// 加法懒标记
if (tr[p].add != 0) {
// 更新左孩子
tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
tr[lc].add += tr[p].add; // 加法标记下传
// 更新右孩子
tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
tr[rc].add += tr[p].add;
// 清除加法懒标记
tr[p].add = 0;
}
}
- 区间更新:给区间 [x,y] 每个数加或乘 k
// 区间乘法
void update_cheng(int p, int x, int y, ll k) {
// 完全覆盖当前区间,直接更新并记录懒标记
if (x <= tr[p].l && tr[p].r <= y) {
tr[p].sum *= k;
tr[p].mul *= k; // 乘法懒标记
tr[p].add *= k; // 加法标记乘以k
return;
}
pushdown(p); // 下传懒标记
int m = (tr[p].l + tr[p].r) >> 1;
if (x <= m) update_cheng(lc, x, y, k); // 更新左子树
if (y > m) update_cheng(rc, x, y, k); // 更新右子树
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新当前节点
}
// 区间加法
void update_jia(int p, int x, int y, ll k) {
// 完全覆盖,直接更新
if (x <= tr[p].l && tr[p].r <= y) {
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].add += k;
return ;
}
pushdown(p);
int m = (tr[p].l + tr[p].r) >> 1;
if (x <= m) update_jia(lc, x, y, k);
if (y > m) update_jia(rc, x, y, k);
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新sum
}
- 完整代码
#include <bits/stdc++.h>
using namespace std;
#define N 100010
#define ll long long
#define lc p << 1 // 左孩子编号:p*2
#define rc p << 1|1 // 右孩子编号:p*2+1
int n, m;
ll w[N]; // 原数组,用ll防止溢出
struct tree {
ll l, r, sum; // 区间左边界、右边界、区间和
ll add, mul; // 加法懒标记、乘法懒标记(补充mul,修复问题3)
};
tree tr[N * 4];
// 懒标记下传
void pushdown(int p) {
// 无孩子
if (tr[p].l == tr[p].r) return;
// 乘法懒标记
if (tr[p].mul != 1) {
// 更新左孩子
tr[lc].sum *= tr[p].mul;
tr[lc].mul *= tr[p].mul; // 乘法标记下传
tr[lc].add *= tr[p].mul; // 加法标记乘以mul
// 更新右孩子
tr[rc].sum *= tr[p].mul;
tr[rc].mul *= tr[p].mul;
tr[rc].add *= tr[p].mul;
// 清除乘法懒标记
tr[p].mul = 1;
}
// 加法懒标记
if (tr[p].add != 0) {
// 更新左孩子
tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
tr[lc].add += tr[p].add; // 加法标记下传
// 更新右孩子
tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
tr[rc].add += tr[p].add;
// 清除加法懒标记
tr[p].add = 0;
}
}
// 构建线段树
void build(int p, int l, int r) {
tr[p] = {l, r, 0, 0, 1};
if (l == r) {
tr[p].sum = w[l];
return;
}
int m = (l + r) >> 1;
build(lc, l, m); // 构建左子树
build(rc, m + 1, r); // 构建右子树
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
// 区间乘法
void update_cheng(int p, int x, int y, ll k) {
// 完全覆盖当前区间,直接更新并记录懒标记
if (x <= tr[p].l && tr[p].r <= y) {
tr[p].sum *= k;
tr[p].mul *= k; // 乘法懒标记
tr[p].add *= k; // 加法标记乘以k
return;
}
pushdown(p); // 下传懒标记
int m = (tr[p].l + tr[p].r) >> 1;
if (x <= m) update_cheng(lc, x, y, k); // 更新左子树
if (y > m) update_cheng(rc, x, y, k); // 更新右子树
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新当前节点
}
// 区间加法
void update_jia(int p, int x, int y, ll k) {
// 完全覆盖,直接更新
if (x <= tr[p].l && tr[p].r <= y) {
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].add += k;
return ;
}
pushdown(p);
int m = (tr[p].l + tr[p].r) >> 1;
if (x <= m) update_jia(lc, x, y, k);
if (y > m) update_jia(rc, x, y, k);
tr[p].sum = tr[lc].sum + tr[rc].sum; // 回溯更新sum
}
// 区间查询
ll query(int p, int x, int y) {
if (x <= tr[p].l && tr[p].r <= y) { // 完全覆盖,直接返回当前区间sum
return tr[p].sum;
}
pushdown(p);
int m = (tr[p].l + tr[p].r) >> 1;
ll sum = 0;
if (x <= m) sum += query(lc, x, y);
if (y > m) sum += query(rc, x, y);
return sum;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i){
cin >> w[i];
}
build(1, 1, n);
for(int i = 1; i <= m; ++i){
int zwy;
cin >> zwy;
if(zwy == 1){
int x, y;
ll k;
cin >> x >> y >> k;
update_cheng(1, x, y, k);
}
else if(zwy == 2){
int x, y;
ll k;
cin >> x >> y >> k;
update_jia(1, x, y, k);
}
else{
int x, y;
cin >> x >> y;
cout << query(1, x, y) << '\n';
}
}
return 0;
}
5. 知识点小结
- 线段树的核心优化是懒标记 (lazy tag),核心思想是延迟更新,将区间修改的时间复杂度从 O(n) 优化为 O(logn)。
- 懒标记的核心操作是pushdown(),只有在需要访问子节点时才下传标记,保证每次操作的效率。
- 单加法懒标记只需维护add,初始化值为 0;加 + 乘复合懒标记需要维护 add 和 mul,初始化mul=1、add=0,且下传顺序必须先乘后加。
- 线段树数组的空间必须开原数组的 4 倍,防止越界。
- 所有区间修改、区间查询操作的递归逻辑一致:完全覆盖则直接处理,否则分裂区间递归处理,最后回溯更新当前节点信息。
- 线段树的核心适用场景:高频的区间修改 + 高频的区间查询类问题。

浙公网安备 33010602011771号