
在掌握了基础线段树、区间修改(单懒标记)后,算法竞赛中常会遇到更复杂的场景 ——多个区间修改操作共存,比如 “区间加 + 区间乘”“区间重置 + 区间加”。此时单懒标记已无法满足需求,核心难点在于确定操作的执行优先级:不同修改操作的顺序会直接影响最终结果,若处理不当,不仅会导致答案错误,还可能引发精度问题。 本文将深入拆解多个区间操作的核心逻辑,聚焦懒标记的优先级设计,通过洛谷经典模板题(区间加 + 区间乘、区间重置 + 区间加),手把手实现带多懒标记的线段树,让你彻底攻克这一进阶难点!下面就让我们正式开始吧!

先看一个直观的例子:假设对数字 x 先执行 “加 3” 再执行 “乘 2”,结果是 (x+3)*2 = 2x+6;若先执行 “乘 2” 再执行 “加 3”,结果是 2x+3。两种顺序的结果天差地别,这就是多个区间操作的核心痛点 ——操作顺序直接影响最终结果。
线段树的多个区间操作,本质是懒标记的叠加与下放。当一个节点同时存在多个懒标记时,必须明确 “先执行哪个操作、后执行哪个操作”,即确定懒标记的优先级。优先级的设计需满足两个原则:
常见的多个区间操作组合及优先级结论:
操作组合 | 优先级结论 | 核心原因 |
|---|---|---|
区间乘(mul)+ 区间加(add) | 先乘后加 | 先乘后加的公式可简化(无精度损失),先加后乘会出现除法,导致精度问题 |
区间重置(set)+ 区间加(add) | 先重置后加 | 重置操作会覆盖之前的所有状态,加操作仅作用于重置后的数值 |
接下来,我们通过两个经典例题,详细讲解不同组合的实现逻辑。
题目链接:https://www.luogu.com.cn/problem/P3373

这是最常见的多区间操作组合,要求支持 “区间乘 k”“区间加 k”“区间求和(取模)” 三种操作。我们先推导优先级公式,再实现完整代码。
假设节点当前的懒标记为 mul0(乘法标记)和 add0(加法标记),表示该节点的所有元素需执行 x = x * mul0 + add0。现在新增一组操作:“乘 m” 和 “加 a”,需要推导新的懒标记 mul1 和 add1。
原操作:x → x * mul0 + add0
新增操作:先乘 m → (x * mul0 + add0) * m,再加 a → (x * mul0 + add0) * m + a
展开后:x * (mul0 * m) + (add0 * m + a)
因此,新的懒标记满足:
mul1 = mul0 * m(取模);add1 = add0 * m + a(取模)。1(因为 x * 1 = x,不影响原始值);0(因为 x + 0 = x,不影响原始值);需要维护的信息:
l/r;sum(需取模);mul(初始 1);add(初始 0)。C++ 代码:
typedef long long LL;
const int N = 1e5 + 10;
int mod; // 模数,由输入指定
struct node {
int l, r;
LL sum, mul, add; // sum:区间和,mul:乘法标记,add:加法标记
} tr[N << 2]; // 线段树空间开4倍 多区间操作的核心是 lazy 函数(更新当前节点标记)和 pushdown 函数(下放标记),需严格遵循 “先乘后加” 的优先级。
基础函数,区间和 = 左孩子和 + 右孩子和(取模):
void pushup(int p) {
tr[p].sum = (tr[p << 1].sum + tr[p << 1 | 1].sum) % mod;
} 接收新增的乘法因子 m 和加法因子 a,根据 “先乘后加” 更新节点信息:
void lazy(int p, LL m, LL a) {
auto& t = tr[p];
// 1. 更新区间和:sum = (sum * m + a * 区间长度) % mod
t.sum = (t.sum * m + a * (t.r - t.l + 1)) % mod;
// 2. 更新乘法标记:mul = (mul * m) % mod
t.mul = t.mul * m % mod;
// 3. 更新加法标记:add = (add * m + a) % mod
t.add = (t.add * m + a) % mod;
}必须先下放乘法标记,再下放加法标记,确保子节点的操作顺序正确:
void pushdown(int p) {
auto& t = tr[p];
// 只有非初始标记时才需要下放(mul≠1 或 add≠0)
if (t.mul != 1 || t.add != 0) {
// 下放标记给左孩子:传递当前节点的 mul 和 add
lazy(p << 1, t.mul, t.add);
// 下放标记给右孩子:传递当前节点的 mul 和 add
lazy(p << 1 | 1, t.mul, t.add);
// 清空当前节点标记(恢复初始状态)
t.mul = 1;
t.add = 0;
}
}初始化节点的区间、和、标记(mul=1,add=0):
void build(int p, int l, int r, LL a[]) {
tr[p] = {l, r, a[l] % mod, 1, 0}; // 初始mul=1,add=0
if (l == r) return;
int mid = (l + r) >> 1;
build(p << 1, l, mid, a);
build(p << 1 | 1, mid + 1, r, a);
pushup(p);
} 根据操作类型,调用 lazy 函数更新标记,核心逻辑与单标记一致:
// 区间修改:[x,y] 执行 乘m + 加a(m=1时仅加,a=0时仅乘)
void modify(int p, int x, int y, LL m, LL a) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
lazy(p, m, a); // 完全覆盖,直接更新标记
return;
}
pushdown(p); // 未完全覆盖,先下放标记
int mid = (t.l + t.r) >> 1;
if (x <= mid) modify(p << 1, x, y, m, a);
if (y > mid) modify(p << 1 | 1, x, y, m, a);
pushup(p); // 整合左右孩子信息
}查询逻辑与单标记一致,需在递归前下放标记:
LL query(int p, int x, int y) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
return t.sum % mod;
}
pushdown(p); // 下放标记,确保子节点信息最新
int mid = (t.l + t.r) >> 1;
LL res = 0;
if (x <= mid) res = (res + query(p << 1, x, y)) % mod;
if (y > mid) res = (res + query(p << 1 | 1, x, y)) % mod;
return res;
}结合题目要求,处理输入输出,支持三种操作:
完整代码:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int mod;
struct node {
int l, r;
LL sum, mul, add;
} tr[N << 2];
void pushup(int p) {
tr[p].sum = (tr[p << 1].sum + tr[p << 1 | 1].sum) % mod;
}
void lazy(int p, LL m, LL a) {
auto& t = tr[p];
t.sum = (t.sum * m + a * (t.r - t.l + 1)) % mod;
t.mul = t.mul * m % mod;
t.add = (t.add * m + a) % mod;
}
void pushdown(int p) {
auto& t = tr[p];
if (t.mul != 1 || t.add != 0) {
lazy(p << 1, t.mul, t.add);
lazy(p << 1 | 1, t.mul, t.add);
t.mul = 1;
t.add = 0;
}
}
void build(int p, int l, int r, LL a[]) {
tr[p] = {l, r, a[l] % mod, 1, 0};
if (l == r) return;
int mid = (l + r) >> 1;
build(p << 1, l, mid, a);
build(p << 1 | 1, mid + 1, r, a);
pushup(p);
}
void modify(int p, int x, int y, LL m, LL a) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
lazy(p, m, a);
return;
}
pushdown(p);
int mid = (t.l + t.r) >> 1;
if (x <= mid) modify(p << 1, x, y, m, a);
if (y > mid) modify(p << 1 | 1, x, y, m, a);
pushup(p);
}
LL query(int p, int x, int y) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
return t.sum % mod;
}
pushdown(p);
int mid = (t.l + t.r) >> 1;
LL res = 0;
if (x <= mid) res = (res + query(p << 1, x, y)) % mod;
if (y > mid) res = (res + query(p << 1 | 1, x, y)) % mod;
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, q;
cin >> n >> q >> mod;
LL a[N];
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] %= mod; // 初始值取模
}
build(1, 1, n, a);
while (q--) {
int op, x, y;
cin >> op >> x >> y;
if (op == 1) {
// 操作1:区间[x,y]乘k
LL k;
cin >> k;
modify(1, x, y, k, 0); // a=0,仅乘
} else if (op == 2) {
// 操作2:区间[x,y]加k
LL k;
cin >> k;
modify(1, x, y, 1, k); // m=1,仅加
} else {
// 操作3:查询区间[x,y]和
cout << query(1, x, y) << endl;
}
}
return 0;
}5 5 38
1 5 4 2 3
2 1 4 1 // 区间[1,4]加1
3 2 5 // 查询[2,5]和
1 2 4 2 // 区间[2,4]乘2
2 3 5 5 // 区间[3,5]加5
3 1 4 // 查询[1,4]和题目链接:https://www.luogu.com.cn/problem/P1253

另一类高频组合是 “区间重置(将区间所有元素改为 x)+ 区间加(将区间所有元素加 x)”。核心优先级是先重置后加—— 重置操作会覆盖之前的所有状态,加操作仅作用于重置后的数值。
假设节点存在 “重置标记” 和 “加法标记”:
x + a;x(加法标记失效)。因此,重置标记的优先级高于加法标记,标记下放时需先传递重置标记,再传递加法标记。同时,重置标记会清空之前的加法标记(因为重置后加法操作需重新计算)。
需要维护的信息:
l/r;max(题目要求查询最大值);add(初始 0);update(初始 0);st(是否有重置标记,初始 false)。C++ 代码:
typedef long long LL;
const int N = 1e6 + 10; // 数据量较大,开1e6+10
struct node {
int l, r;
LL max, add, update;
bool st; // st=true表示有重置标记
} tr[N << 2];void pushup(int p) {
tr[p].max = max(tr[p << 1].max, tr[p << 1 | 1].max);
}根据操作类型(重置 / 加)更新节点信息,遵循 “先重置后加”:
// st:是否重置,update:重置值,add:增加值
void lazy(int p, bool st, LL update_val, LL add_val) {
auto& t = tr[p];
// 1. 先处理重置(优先级更高)
if (st) {
t.max = update_val; // 重置为指定值
t.add = 0; // 重置后清空加法标记
t.update = update_val;
t.st = true;
}
// 2. 再处理加法
t.max += add_val;
t.add += add_val;
}先下放重置标记,再下放加法标记,确保子节点操作顺序正确:
void pushdown(int p) {
auto& t = tr[p];
// 有重置标记或加法标记时下放
if (t.st || t.add != 0) {
// 下放给左孩子
lazy(p << 1, t.st, t.update, t.add);
// 下放给右孩子
lazy(p << 1 | 1, t.st, t.update, t.add);
// 清空当前节点标记
t.st = false;
t.update = 0;
t.add = 0;
}
}void build(int p, int l, int r, LL a[]) {
tr[p] = {l, r, a[l], 0, 0, false};
if (l == r) return;
int mid = (l + r) >> 1;
build(p << 1, l, mid, a);
build(p << 1 | 1, mid + 1, r, a);
pushup(p);
}// st:true=重置,false=加法;val:重置值/增加值
void modify(int p, int x, int y, bool st, LL val) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
if (st) {
// 重置操作:add_val=0
lazy(p, true, val, 0);
} else {
// 加法操作:st=false,update_val=0
lazy(p, false, 0, val);
}
return;
}
pushdown(p);
int mid = (t.l + t.r) >> 1;
if (x <= mid) modify(p << 1, x, y, st, val);
if (y > mid) modify(p << 1 | 1, x, y, st, val);
pushup(p);
}LL query(int p, int x, int y) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
return t.max;
}
pushdown(p);
int mid = (t.l + t.r) >> 1;
LL res = -1e18; // 初始值设为极小值
if (x <= mid) res = max(res, query(p << 1, x, y));
if (y > mid) res = max(res, query(p << 1 | 1, x, y));
return res;
}题目要求支持三种操作:
完整代码:
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
struct node {
int l, r;
LL max, add, update;
bool st;
} tr[N << 2];
void pushup(int p) {
tr[p].max = max(tr[p << 1].max, tr[p << 1 | 1].max);
}
void lazy(int p, bool st, LL update_val, LL add_val) {
auto& t = tr[p];
if (st) {
t.max = update_val;
t.add = 0;
t.update = update_val;
t.st = true;
}
t.max += add_val;
t.add += add_val;
}
void pushdown(int p) {
auto& t = tr[p];
if (t.st || t.add != 0) {
lazy(p << 1, t.st, t.update, t.add);
lazy(p << 1 | 1, t.st, t.update, t.add);
t.st = false;
t.update = 0;
t.add = 0;
}
}
void build(int p, int l, int r, LL a[]) {
tr[p] = {l, r, a[l], 0, 0, false};
if (l == r) return;
int mid = (l + r) >> 1;
build(p << 1, l, mid, a);
build(p << 1 | 1, mid + 1, r, a);
pushup(p);
}
void modify(int p, int x, int y, bool st, LL val) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
if (st) {
lazy(p, true, val, 0);
} else {
lazy(p, false, 0, val);
}
return;
}
pushdown(p);
int mid = (t.l + t.r) >> 1;
if (x <= mid) modify(p << 1, x, y, st, val);
if (y > mid) modify(p << 1 | 1, x, y, st, val);
pushup(p);
}
LL query(int p, int x, int y) {
auto& t = tr[p];
if (x <= t.l && t.r <= y) {
return t.max;
}
pushdown(p);
int mid = (t.l + t.r) >> 1;
LL res = -1e18;
if (x <= mid) res = max(res, query(p << 1, x, y));
if (y > mid) res = max(res, query(p << 1 | 1, x, y));
return res;
}
int main() {
int n, q;
scanf("%d%d", &n, &q); // 用scanf加速输入
LL a[N];
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
}
build(1, 1, n, a);
while (q--) {
int op, l, r;
scanf("%d%d%d", &op, &l, &r);
if (op == 1) {
// 操作1:重置为x
LL x;
scanf("%lld", &x);
modify(1, l, r, true, x);
} else if (op == 2) {
// 操作2:加x
LL x;
scanf("%lld", &x);
modify(1, l, r, false, x);
} else {
// 操作3:查询最大值
printf("%lld\n", query(1, l, r));
}
}
return 0;
}6 6
1 1 4 5 1 4
1 1 2 6 // 区间[1,2]重置为6
2 3 4 2 // 区间[3,4]加2
3 1 4 // 查询[1,4]最大值
3 2 3 // 查询[2,3]最大值
1 1 6 -1 // 区间[1,6]重置为-1
3 1 6 // 查询[1,6]最大值通过以上两个例子,我们可以总结出多区间操作的通用解题框架,无论遇到哪种组合,都可以按以下步骤思考:
这是最核心的一步,需明确不同操作的执行顺序。常见优先级规则:
多区间操作的代码复杂度较高,新手容易踩坑,以下是五大高频易错点:
这是最致命的错误!比如 “先加后乘”“先加后重置”,会导致结果错误或精度损失。解决方法:先推导公式,再编写代码,确保 lazy 和 pushdown 的操作顺序一致。
pushdown 后未将当前节点的标记恢复为初始状态,导致标记被重复下放,多次执行同一操作。
重置操作会覆盖之前的所有状态,若未清空加法标记,后续加法会作用于重置前的数值,导致结果错误。
区间和、最大值等可能超过 int 范围,必须用 long long 存储,避免溢出。
多区间操作的线段树空间仍需开 4 倍原始数组大小,否则会出现数组越界(尤其是数据量达 1e6 时)。
多个区间操作的核心是懒标记的优先级设计,只要明确操作顺序、推导正确的叠加公式,再遵循通用解题框架,就能轻松实现。本文通过两个经典例题,详细讲解了 “区间乘 + 区间加”“区间重置 + 区间加” 的实现逻辑,涵盖了公式推导、结构体设计、核心函数编写、完整代码实现等环节,帮助大家彻底攻克这一进阶难点。 线段树的灵活性在于其可维护的信息种类繁多,而多区间操作是线段树进阶的必经之路。掌握了多懒标记的处理方法,你就能应对算法竞赛中绝大多数区间操作问题。建议大家多刷题、多推导,加深对优先级的理解,真正做到举一反三! 创作不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连~