题解:P3373 【模板】线段树 2
Chase12345 · · 题解
引入
这道题我们需要维护区间加、区间乘、区间和,由于序列长度和询问次数都比较大,暴力显然不行。那么我们这里需要一种数据结构,维护区间。这正是线段树所能维护的东西。
介绍
线段树是一种二叉树数据结构,用于高效处理区间查询和区间更新操作。它将区间递归地划分为子区间,每个节点代表一个区间,并存储该区间的相关信息(如区间和、最大值等)。
对于长度为
懒标记
懒标记介绍
对于每个修改操作,如果我们将所有标记下传到叶子,这会导致时间复杂度爆炸。所以我们能否有更优的标记方式?正如我所说,这就是懒标记。对于每个节点,我们维护它的乘法标记和加法标记,每次查询或修改需要用到这个标记之后再下传即可。由于我们每次操作都是从树根开始操作,所以每个标记都必定会正确传到某个节点。这样使得标记的时间复杂度是常数级别。
懒标记的顺序
这里懒标记的顺序并不能交换。当只有乘法标记或者加法标记的时候,直接下传。如果都有,请注意,需要先传乘法再传加法。证明如下:
假设有一个值
x ,我们先加a 再乘m ,结果是(x + a) \times m 。而如果先乘m 再加a ,结果是x \times m + a ,这显然不等价。那么必须要通过先乘再加的传递方式进行传递。操作方法
所有的操作我们都使用同样的结构:
- 如果当前节点区间完全包含在目标区间内,直接更新或返回结果。
- 否则,先下放标记,然后递归处理子节点。
- 最后合并子节点的结果。
时间复杂度
如上已经分析。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
struct Tree {
int l, r;
long long sum, add, mul;
}tree[N << 2];
long long n, q, m, a[N];
void build(int p, int l, int r) {
tree[p].l = l;
tree[p].r = r;
tree[p].mul = 1;
if (l == r) {
tree[p].sum = a[l];
return;
}
int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
tree[p].sum = tree[p << 1].sum + tree[p << 1 | 1].sum;
}
void spread(int p) {
if (tree[p].mul != 1) {
tree[p << 1].sum = tree[p << 1].sum * tree[p].mul % m;
tree[p << 1].add = tree[p << 1].add * tree[p].mul % m;
tree[p << 1].mul = tree[p << 1].mul * tree[p].mul % m;
tree[p << 1 | 1].sum = tree[p << 1 | 1].sum * tree[p].mul % m;
tree[p << 1 | 1].add = tree[p << 1 | 1].add * tree[p].mul % m;
tree[p << 1 | 1].mul = tree[p << 1 | 1].mul * tree[p].mul % m;
tree[p].mul = 1;
}
if (tree[p].add) {
tree[p << 1].sum = (tree[p << 1].sum + tree[p].add * (tree[p << 1].r - tree[p << 1].l + 1)) % m;
tree[p << 1].add = (tree[p << 1].add + tree[p].add) % m;
tree[p << 1 | 1].sum = (tree[p << 1 | 1].sum + tree[p].add * (tree[p << 1 | 1].r - tree[p << 1 | 1].l + 1)) % m;
tree[p << 1 | 1].add = (tree[p << 1 | 1].add + tree[p].add) % m;
tree[p].add = 0;
}
}
void add(int p, int l, int r, long long k) {
if (tree[p].r < l || tree[p].l > r)
return;
if (l <= tree[p].l && tree[p].r <= r) {
tree[p].sum = (tree[p].sum + k * (tree[p].r - tree[p].l + 1)) % m;
tree[p].add = (tree[p].add + k) % m;
return;
}
spread(p);
add(p << 1, l, r, k);
add(p << 1 | 1, l, r, k);
tree[p].sum = (tree[p << 1].sum + tree[p << 1 | 1].sum) % m;
}
void mul(int p, int l, int r, long long k) {
if (tree[p].r < l || tree[p].l > r)
return;
if (l <= tree[p].l && tree[p].r <= r) {
tree[p].sum = tree[p].sum * k % m;
tree[p].add = tree[p].add * k % m;
tree[p].mul = tree[p].mul * k % m;
return;
}
spread(p);
mul(p << 1, l, r, k);
mul(p << 1 | 1, l, r, k);
tree[p].sum = (tree[p << 1].sum + tree[p << 1 | 1].sum) % m;
}
long long query(int p, int l, int r) {
if (tree[p].r < l || tree[p].l > r)
return 0;
if (l <= tree[p].l && tree[p].r <= r)
return tree[p].sum % m;
spread(p);
return (query(p << 1, l, r) + query(p << 1 | 1, l, r)) % m;
}
int main() {
cin >> n >> q >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
build(1, 1, n);
while (q--) {
int op, l, r;
long long k;
cin >> op >> l >> r;
if (op == 1) {
cin >> k;
mul(1, l, r, k);
} else if (op == 2) {
cin >> k;
add(1, l, r, k);
} else
cout << query(1, l, r) << '\n';
}
}
提醒
在一道题中,树状数组的常数相对线段树小很多,如果树状数组比较好实现,最好不要用线段树。容易被卡常。
线段树空间需要开到
Update:
懒标记传递顺序有笔误,现已修改。