
很好小朋友们,我们被要求写一个可持久化线段树,支持区间修改、区间复制和输出区间和。
懒标记是必须的,但可持久化线段树一般不太支持 pushdown,尤其在本题中。
在进入子节点之前,我们需要**克隆当前节点,**因为当前节点可能被其他版本共享。
然后才能将其懒标记下推到新克隆出的子节点,最后将当前节点的懒标记重置。
这样会导致不必要的新节点开销。
于是我们不考虑 pushdown,而是在递归函数中同时传进祖先变化,即懒标记。
线段树维护五个参数,分别是左右子节点、区间总和 b、c 懒标记。
(1)处理操作 0
我们构建一个 change 函数,对版本 p 的区间 l, r 施加变换 x -> b*x + c,返回新版本的根节点。
可持久化的精髓是"改了才新建",编写时请注意祖先变化的传递。
(2)处理操作 1
一个个复制绝对是不明智的,我们可以先构建一个 crossover 函数。
合并两个版本 x 和 y,返回一个新版本,其前 at 个元素来自 x,后 len - at 个元素来自 y。
这样题目的 l, r 可以变成先处理 1, r 的复制,接着把原来的 1, l - 1 复制回去。
(3)处理操作 2
正常的 query 函数即可。
另注意:本题的边界和 base 十分复杂,我用的是统一 base-1。
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL P = 998244353; // 模数
const int N = 1e5 + 10; // 数组最大长度
#define lc(p) tr[p].lc // 左孩子下标
#define rc(p) tr[p].rc // 右孩子下标
#define MID ((L + R) >> 1) // 当前区间中点
/*
线段树节点结构:
lc, rc : 左右孩子指针(下标)
sum : 当前节点代表的区间和(已应用该节点的懒标记)
lazy_b, lazy_c : 懒标记,表示对子区间施加的仿射变换 x -> lazy_b * x + lazy_c
*/
struct node {
int lc, rc;
LL sum;
LL lazy_b, lazy_c;
};
vector<node> tr; // 动态存储所有节点,0 号节点为空节点
LL a[N]; // 原始数组(1‑based)
// 新建一个节点,懒标记初始为恒等变换 (1, 0)
int newd() {
tr.push_back({0, 0, 0, 1, 0});
return (int)tr.size() - 1;
}
// 克隆节点 p,复制其所有信息
int clone_node(int p) {
tr.push_back(tr[p]);
return (int)tr.size() - 1;
}
/*
将仿射变换 x -> b*x + c 应用到节点 p 所代表的整个区间(长度为 len)
同时更新该节点的懒标记,使其与原有懒标记复合。
注意:应用顺序为 新变换 作用于 旧变换,即最终变换为 b*(old(x)) + c。
*/
void modify(int p, LL b, LL c, LL len) {
if (p == 0) return;
tr[p].sum = (b * tr[p].sum + c * len) % P;
tr[p].lazy_b = b * tr[p].lazy_b % P;
tr[p].lazy_c = (b * tr[p].lazy_c % P + c) % P;
}
// 复合变换:返回 g(f()),即先 f 后 g
pair<LL, LL> compose(LL g_b, LL g_c, LL f_b, LL f_c) {
return { (g_b * f_b) % P, (g_b * f_c + g_c) % P };
}
// 用左右子树的 sum 更新当前节点的 sum
void pushup(int p) {
tr[p].sum = (tr[lc(p)].sum + tr[rc(p)].sum) % P;
}
// 建树,p 为引用返回根节点下标
void build(int &p, int L, int R) {
p = newd();
if (L == R) {
tr[p].sum = a[L] % P;
return;
}
build(lc(p), L, MID);
build(rc(p), MID + 1, R);
pushup(p);
}
/*
对版本 p 的区间 [l, r] 施加变换 x -> b*x + c,返回新版本的根节点。
参数 prop_b, prop_c 表示从根到当前节点路径上所有祖先懒标记复合后的变换,
需要在访问当前区间时应用。
本函数采用路径复制,只修改必要的节点,保留未修改部分。
*/
int change(int p, int L, int R, int l, int r, LL b, LL c, LL prop_b, LL prop_c) {
// 完全不相交:克隆当前节点并应用祖先变换,然后返回
if (r < L || R < l) {
int np = clone_node(p);
modify(np, prop_b, prop_c, R - L + 1);
return np;
}
// 完全覆盖:克隆当前节点,整体施加 "祖先变换 + 当前变换"
if (l <= L && R <= r) {
auto t = compose(b, c, prop_b, prop_c); // 先 prop,后 (b,c)
int np = clone_node(p);
modify(np, t.first, t.second, R - L + 1);
return np;
}
// 部分重叠:创建新节点,懒标记为恒等
int np = newd();
// 传递给子节点的祖先变换 = prop ∘ p.lazy(先 p.lazy,后 prop)
auto t = compose(prop_b, prop_c, tr[p].lazy_b, tr[p].lazy_c);
// 左子区间有重叠,递归处理
if (l <= MID) {
lc(np) = change(lc(p), L, MID, l, r, b, c, t.first, t.second);
}
else {
// 左子区间没有重叠,克隆并应用祖先变换
if (lc(p) != 0) {
int lc = clone_node(lc(p));
modify(lc, t.first, t.second, MID - L + 1);
lc(np) = lc;
}
}
// 右子区间有重叠,递归处理
if (r >= MID + 1) {
rc(np) = change(rc(p), MID + 1, R, l, r, b, c, t.first, t.second);
}
else {
if (rc(p) != 0) {
int rc = clone_node(rc(p));
modify(rc, t.first, t.second, R - (MID + 1) + 1);
rc(np) = rc;
}
}
pushup(np);
return np;
}
// 封装 update - change,初始祖先变换为恒等
int update(int p, int L, int R, int l, int r, LL b, LL c) {
return change(p, L, R, l, r, b, c, 1, 0);
}
/*
合并两个版本 x 和 y,返回一个新版本,其前 at 个元素来自 x,后 len - at 个元素来自 y。
参数 xb,xc 和 yb,yc 分别为两个版本当前需要应用的祖先变换。
具体规则:
- at <= 0 :全部取 y
- at >= len :全部取 x
- 否则根据 at 与左子树长度的关系递归处理左右子树。
*/
int crossover(int x, int y, int len, int at, LL xb, LL xc, LL yb, LL yc) {
if (at <= 0) { // 全部来自 y
if (y == 0) return 0;
int ny = clone_node(y);
modify(ny, yb, yc, len);
return ny;
}
if (len <= at) { // 全部来自 x
if (x == 0) return 0;
int nx = clone_node(x);
modify(nx, xb, xc, len);
return nx;
}
int midl = (len + 1) >> 1; // 左子树长度(尽量平衡)
// 为啥是上取整?len = R - L + 1
// MID = (R + L) / 2
// midl = MID - L + 1
// 2 * midl = R + L - 2 * L + 2 * 1
// midl = R - L + 1
LL nx_b, nx_c, ny_b, ny_c;
// 将 x 的祖先变换与其自身懒标记复合,得到传递给左/右子树的变换
if (x != 0) {
auto res = compose(xb, xc, tr[x].lazy_b, tr[x].lazy_c);
nx_b = res.first; nx_c = res.second;
}
else {
nx_b = xb; nx_c = xc;
}
if (y != 0) {
auto res = compose(yb, yc, tr[y].lazy_b, tr[y].lazy_c);
ny_b = res.first; ny_c = res.second;
}
else {
ny_b = yb; ny_c = yc;
}
int left_x = (x == 0) ? 0 : lc(x);
int left_y = (y == 0) ? 0 : lc(y);
int right_x = (x == 0) ? 0 : rc(x);
int right_y = (y == 0) ? 0 : rc(y);
// 左子树需要的前 at 个元素来自 x
int lc = crossover(left_x, left_y, midl, at,
nx_b, nx_c, ny_b, ny_c);
// 右子树需要的前 at - midl 个元素来自 x
int rc = crossover(right_x, right_y, len - midl, at - midl,
nx_b, nx_c, ny_b, ny_c);
int res = newd();
lc(res) = lc;
rc(res) = rc;
pushup(res);
return res;
}
/*
操作 1 的封装:将版本 y 的区间 [l, r] 复制到版本 x 的对应位置。
通过两次交叉实现:
1. tmp = x[0 : l - 1] + y[l : n]
2. 结果 = tmp[0 : r] + x[r : n] = x[0 : l - 1] + y[l : r] + x[r : n]
*/
int update_crossover_twice(int x, int y, int l, int r, int total_len) {
int tmp = crossover(x, y, total_len, l - 1, 1, 0, 1, 0);
return crossover(tmp, x, total_len, r, 1, 0, 1, 0);
}
/*
查询版本 p 中区间 [l, r] 的和。
参数 b, c 表示当前路径上所有祖先懒标记复合后的变换,
在完全覆盖时直接应用到当前节点的 sum 上。
*/
LL query(int p, int L, int R, int l, int r, LL b, LL c) {
if (r < L || R < l) {
return 0;
}
if (l <= L && R <= r) {
return (b * tr[p].sum + c * (R - L + 1)) % P;
}
// 传递给子节点的祖先变换 = (b,c) ∘ p.lazy(先 p.lazy,后当前祖先)
auto t = compose(b, c, tr[p].lazy_b, tr[p].lazy_c);
LL res = 0;
res += query(lc(p), L, MID, l, r, t.first, t.second);
res += query(rc(p), MID + 1, R, l, r, t.first, t.second);
return res % P;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, Q;
cin >> n >> Q;
for (int i = 1; i <= n; i ++) cin >> a[i];
tr.reserve(20000000); // 预留节点空间
tr.push_back({0, 0, 0, 1, 0}); // 0 号节点作为空节点
int rt;
build(rt, 1, n);
vector<int> roots(Q + 1); // roots[i] 存储版本 A_i 的根节点
roots[0] = rt; // 初始版本 A_{-1}
for (int i = 1; i <= Q; i++) {
int opt;
cin >> opt;
// 输入中 k, s 为 0‑based 版本编号,-1 表示初始版本,所以用 k+1 索引 roots
if (opt == 0) { // 区间仿射变换
int k, l, r;
LL b, c;
cin >> k >> l >> r >> b >> c;
l ++; // 输入区间为 [l, r) 的 0‑based 下标,转为 1‑based
int src = roots[k + 1];
roots[i] = update(src, 1, n, l, r, b, c);
}
else if (opt == 1) { // 区间从另一版本复制
int k, s, l, r;
cin >> k >> s >> l >> r;
l ++;
int src_k = roots[k + 1];
int src_s = roots[s + 1];
roots[i] = update_crossover_twice(src_k, src_s, l, r, n);
}
else { // 区间求和查询
int k, l, r;
cin >> k >> l >> r;
l ++;
int src = roots[k + 1];
cout << query(src, 1, n, l, r, 1, 0) << "\n";
roots[i] = roots[i - 1];
}
}
return 0;
}