线段树的区间(加、乘)修改、区间询问

线段树的区间(加、乘)修改、区间询问


学习是一个持续的过程,每一小步都是进步。

———— 我也不知道是谁说的


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. 区间加、乘法

  • 下传懒标记:将当前节点的 muladd 标记传递给左右子节点
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;加 + 乘复合懒标记需要维护 addmul,初始化mul=1add=0,且下传顺序必须先乘后加。
  • 线段树数组的空间必须开原数组的 4 倍,防止越界。
  • 所有区间修改、区间查询操作的递归逻辑一致:完全覆盖则直接处理,否则分裂区间递归处理,最后回溯更新当前节点信息。
  • 线段树的核心适用场景:高频的区间修改 + 高频的区间查询类问题。
posted @ 2026-01-17 21:22  sand_and_water  阅读(0)  评论(0)    收藏  举报