题解:AcWing 1277 维护序列
【题目来源】
AcWing:1277. 维护序列 - AcWing题库
【题目描述】
老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。
有长为 \(N\) 的数列,不妨设为 \(a_1,a_2,…,a_N\)。
有如下三种操作形式:
- 把数列中的一段数全部乘一个值;
- 把数列中的一段数全部加一个值;
- 询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模 \(P\) 的值。
【输入】
第一行两个整数 \(N\) 和 \(P\);
第二行含有 \(N\) 个非负整数,从左到右依次为 \(a_1,a_2,…,a_N\);
第三行有一个整数 \(M\),表示操作总数;
从第四行开始每行描述一个操作,输入的操作有以下三种形式:
- 操作 \(1\):
1 t g c,表示把所有满足 \(t≤i≤g\) 的 \(a_i\) 改为 \(a_i×c\); - 操作 \(2\):
2 t g c,表示把所有满足 \(t≤i≤g\) 的 \(a_i\) 改为 \(a_i+c\); - 操作 \(3\):
3 t g,询问所有满足 \(t≤i≤g\) 的 \(a_i\) 的和模 \(P\) 的值。
同一行相邻两数之间用一个空格隔开,每行开头和末尾没有多余空格。
【输出】
对每个操作 \(3\),按照它在输入中出现的顺序,依次输出一行一个整数表示询问结果。
【输入样例】
7 43
1 2 3 4 5 6 7
5
1 2 5 5
3 2 4
2 3 7 9
3 1 3
3 4 7
【输出样例】
2
35
8
【核心思想】
-
问题分析:给定长度为 \(N\) 的数列 \(a_1, a_2, \ldots, a_N\),需要支持两种区间修改操作(区间乘 \(c\)、区间加 \(c\))和一种区间查询操作(区间和模 \(P\))。这是一个线段树 + 懒标记(Lazy Propagation)问题,核心挑战在于两种修改操作的叠加与优先级处理。
-
算法选择:
- 线段树(Segment Tree):将数列划分为 \(O(\log N)\) 层区间结构,支持区间修改和查询的 \(O(\log N)\) 时间复杂度
- 懒标记(Lazy Tag):延迟下放修改信息,将多次区间修改的复杂度从 \(O(N)\) 降至 \(O(\log N)\)
- 双懒标记维护:同时维护加法懒标记 \(add\) 和乘法懒标记 \(mul\),处理"先乘后加"的复合操作
-
关键步骤:
- 建树:递归构建线段树,每个节点存储区间 \([l, r]\)、区间和 \(sum\)、加法懒标记 \(add = 0\)、乘法懒标记 \(mul = 1\)
- 懒标记应用(eval):对节点区间 \([l, r]\) 应用操作 \((add, mul)\) 时:
- 更新区间和:\(sum \leftarrow sum \times mul + (r - l + 1) \times add\)
- 更新懒标记:\(mul \leftarrow mul_{new} \times mul_{old}\),\(add \leftarrow add_{old} \times mul_{new} + add_{new}\)
- 核心原理:先乘后加,新操作叠加在旧操作之上,保持运算顺序
- 懒标记下传(pushdown):将父节点的 \((add, mul)\) 下传给左右子节点后,清空父节点懒标记(\(add \leftarrow 0, mul \leftarrow 1\))
- 区间修改(update):
- 若当前节点区间被完全包含,直接应用懒标记
- 否则先下传懒标记,再递归处理左右子树,最后向上更新
- 区间查询(query):
- 若当前节点区间被完全包含,直接返回 \(sum\)
- 否则先下传懒标记,再递归查询左右子树并合并结果
- 模数处理:所有运算结果对 \(P\) 取模,防止溢出
-
时间/空间复杂度:
- 时间复杂度:\(O(M \log N)\),每次区间修改和查询均为 \(O(\log N)\),\(M\) 次操作总计 \(O(M \log N)\)
- 空间复杂度:\(O(N)\),线段树开 \(4N\) 空间存储节点信息
-
线段树懒标记的核心思想:
- 延迟下放:修改操作不立即更新到每个叶子节点,而是记录在对应区间节点上,查询时再按需下传,将多次修改的复杂度从 \(O(N)\) 优化到 \(O(\log N)\)
- 标记叠加与优先级:当已有懒标记 \((add_{old}, mul_{old})\) 时,新操作 \((add_{new}, mul_{new})\) 的叠加公式为 \(mul \leftarrow mul_{old} \times mul_{new}\),\(add \leftarrow add_{old} \times mul_{new} + add_{new}\),确保"先乘后加"的运算顺序
- 完全覆盖剪枝:若当前节点区间被操作区间完全包含,直接在该节点打标记并停止递归,避免不必要的下传
- 适用于区间修改 + 区间查询类问题,尤其是涉及多种操作叠加的场景
【算法标签】
线段树
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100005, INF = 1e18; // N: 最大数列长度; INF: 无穷大
int n, m, p; // n: 数列长度; m: 操作次数; p: 模数
int w[N]; // w[i]: 数列初始值
struct Node // 线段树节点结构体
{
int l, r; // l: 区间左端点; r: 区间右端点
int sum, add, mul; // sum: 区间和; add: 加法懒标记; mul: 乘法懒标记
} tr[N * 4]; // 线段树数组,开 4 倍空间
void pushup(int u) // 由子节点信息计算父节点信息(向上更新)
{
tr[u].sum = (tr[u << 1].sum + tr[u << 1 | 1].sum) % p;
}
void eval(Node &t, int add, int mul) // 对节点 t 应用加法 add 和乘法 mul 的懒标记
{
t.sum = (t.sum * mul + (t.r - t.l + 1) * add) % p; // 更新区间和
t.mul = t.mul * mul % p; // 更新乘法懒标记
t.add = (t.add * mul + add) % p; // 更新加法懒标记(先乘后加)
}
void pushdown(int u) // 将父节点的懒标记下传给子节点
{
eval(tr[u << 1], tr[u].add, tr[u].mul); // 下传给左子树
eval(tr[u << 1 | 1], tr[u].add, tr[u].mul); // 下传给右子树
tr[u].add = 0, tr[u].mul = 1; // 清空父节点的懒标记
}
void build(int u, int l, int r) // 建立线段树,u: 节点编号,[l,r]: 节点区间
{
if (l == r) // 叶子节点
tr[u] = {l, r, w[r], 0, 1}; // 初始化:sum=初始值,add=0,mul=1
else
{
tr[u] = {l, r, 0, 0, 1}; // 初始化内部节点
int mid = l + r >> 1; // 计算区间中点
build(u << 1, l, mid); // 递归建立左子树
build(u << 1 | 1, mid + 1, r); // 递归建立右子树
pushup(u); // 向上更新父节点
}
}
void update(int u, int l, int r, int add, int mul) // 区间更新,[l,r] 加 add 乘 mul
{
if (tr[u].l >= l && tr[u].r <= r) // 当前节点区间被完全包含
{
eval(tr[u], add, mul); // 直接对当前节点应用懒标记
}
else
{
pushdown(u); // 下传懒标记后再递归
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) // 左子树有交集
update(u << 1, l, r, add, mul);
if (r > mid) // 右子树有交集
update(u << 1 | 1, l, r, add, mul);
pushup(u); // 向上更新父节点
}
}
int query(int u, int l, int r) // 区间查询,[l,r] 的和
{
if (tr[u].l >= l && tr[u].r <= r) // 当前节点区间被完全包含
{
return tr[u].sum; // 直接返回区间和
}
else
{
pushdown(u); // 下传懒标记
int mid = tr[u].l + tr[u].r >> 1;
int sum = 0;
if (l <= mid) // 左子树有交集
sum = query(u << 1, l, r);
if (r > mid) // 右子树有交集
sum = (sum + query(u << 1 | 1, l, r)) % p;
return sum;
}
}
signed main()
{
cin >> n >> p; // 读入数列长度和模数
for (int i = 1; i <= n; i++) // 读入数列初始值
cin >> w[i];
build(1, 1, n); // 建立线段树
cin >> m; // 读入操作次数
while (m--) // 循环处理 m 次操作
{
int t, l, r, d; // t: 操作类型; l,r: 区间; d: 操作数值
cin >> t >> l >> r;
if (t == 1) // 操作 1:区间乘法
{
cin >> d;
update(1, l, r, 0, d); // add=0, mul=d
}
else if (t == 2) // 操作 2:区间加法
{
cin >> d;
update(1, l, r, d, 1); // add=d, mul=1
}
else // 操作 3:区间求和
cout << query(1, l, r) << endl;
}
return 0;
}
【运行结果】
7 43
1 2 3 4 5 6 7
5
1 2 5 5
3 2 4
2 3 7 9
3 1 3
3 4 7
2
35
8
浙公网安备 33010602011771号