## [【模板】线段树 2](https://www.luogu.com.cn/problem/P3373) 如题,已知一个数列,你需要进行下面三种操作: - 将某区间每一个数乘上 $x$; - 将某区间每一个数加上 $x$; - 求出某区间每一个数的和。 #### 输入格式 第一行包含三个整数 $n,q,m$,分别表示该数列数字的个数、操作的总个数和模数。 第二行包含 $n$ 个用空格分隔的整数,其中第 $i$ 个数字表示数列第 $i$ 项的初始值。 接下来 $q$ 行每行包含若干个整数,表示一个操作,具体如下: 操作 $1$: 格式:`1 x y k` 含义:将区间 $[x,y]$ 内每个数乘上 $k$ 操作 $2$: 格式:`2 x y k` 含义:将区间 $[x,y]$ 内每个数加上 $k$ 操作 $3$: 格式:`3 x y` 含义:输出区间 $[x,y]$ 内每个数的和对 $m$ 取模所得的结果 #### 输出格式 输出包含若干行整数,即为所有操作 $3$ 的结果。 #### 样例输入 #1 ``` 5 5 38 1 5 4 2 3 2 1 4 1 3 2 5 1 2 4 2 2 3 5 5 3 1 4 ``` #### 样例输出 #1 ``` 17 2 ``` #### 提示 【数据范围】 对于 $30\%$ 的数据:$n \le 8$,$q \le 10$。 对于 $70\%$ 的数据:$n \le 10^3 $,$q \le 10^4$。 对于 $100\%$ 的数据:$1 \le n \le 10^5$,$1 \le q \le 10^5$。 除样例外,$m = 571373$。 (数据已经过加强 ^\_^) 样例说明: ![](https://cdn.luogu.com.cn/upload/pic/2255.png) 故输出应为 $17$、$2$($40 \bmod 38 = 2$)。 ### 解题思路 那么这道题有些颠覆了我对懒标记的认识,首先 **加和乘混在一起** 肯定要 **注意次序**,那我们就以先加再乘为例,原$a_1,a_2,a_3,\dots,a_n$变为$a_1+t,a_2+t,a_3+t,\dots,a_n+t$,乘上$x$后变为$a_1x+tx,a_2x+tx,a_3x+tx,\dots,a_nx+tx$,那可以发现这是一个乘法分配律的过程,那么对于加的懒标记不仅要加上$t$,而且要乘$x$,而对于乘的懒标记则要乘上$x$。 #### 线段树解法 ```cpp {.line-numbers} #include using namespace std; const int N = 1000010; int n, m, p; // 线段树模板 #define LL long long #define ls u << 1 #define rs u << 1 | 1 #define mid ((l + r) >> 1) struct Node { int l, r; int mu, add; LL sum; } tr[N << 2]; void pushup(int u) { tr[u].sum = (tr[ls].sum + tr[rs].sum) % p; } void build(int u, int l, int r) { tr[u].l = l, tr[u].r = r, tr[u].mu = 1; if (l == r) { cin >> tr[u].sum; return; } build(ls, l, mid), build(rs, mid + 1, r); pushup(u); } void pushdown(int u) { if (tr[u].add == 0 && tr[u].mu == 1) return; // 默认懒标记 int &mu = tr[u].mu, &add = tr[u].add; // 此时的add懒标记,已经处理过了 tr[ls].sum = ((LL)mu * tr[ls].sum % p + (LL)(tr[ls].r - tr[ls].l + 1) * add % p) % p; tr[rs].sum = ((LL)mu * tr[rs].sum % p + (LL)(tr[rs].r - tr[rs].l + 1) * add % p) % p; tr[ls].mu = (LL)tr[ls].mu * mu % p; tr[rs].mu = (LL)tr[rs].mu * mu % p; tr[ls].add = ((LL)tr[ls].add * mu % p + add) % p; tr[rs].add = ((LL)tr[rs].add * mu % p + add) % p; mu = 1, add = 0; // 清空懒标记 } void add(int u, int L, int R, int v) { int l = tr[u].l, r = tr[u].r; if (l >= L && r <= R) { tr[u].add = ((LL)tr[u].add + v) % p; tr[u].sum = ((LL)tr[u].sum + v * ((LL)tr[u].r - tr[u].l + 1) % p) % p; return; } if (l > R || r < L) return; pushdown(u); add(ls, L, R, v), add(rs, L, R, v); pushup(u); } void mu(int u, int L, int R, int v) { int l = tr[u].l, r = tr[u].r; if (l >= L && r <= R) { tr[u].add = (LL)tr[u].add * v % p; // 比较重要的一步,add要在这里乘上v,因为后面可能要加其他的数而那些数其实是不用乘k的 tr[u].mu = (LL)tr[u].mu * v % p; tr[u].sum = (LL)tr[u].sum * v % p; return; } if (l > R || r < L) return; pushdown(u); mu(ls, L, R, v), mu(rs, L, R, v); pushup(u); } LL query(int u, int L, int R) { int l = tr[u].l, r = tr[u].r; if (l >= L && r <= R) return tr[u].sum; if (l > R || r < L) return 0; pushdown(u); return (query(ls, L, R) + query(rs, L, R)) % p; } signed main() { // 文件输入输出 #ifndef ONLINE_JUDGE freopen("P3373.in", "r", stdin); #endif // 加快读入 ios::sync_with_stdio(false), cin.tie(0); cin >> n >> m >> p; build(1, 1, n); while (m--) { int op, l, r; cin >> op >> l >> r; if (op == 1) { int k; cin >> k; mu(1, l, r, k); } else if (op == 2) { int k; cin >> k; add(1, l, r, k); } else printf("%lld\n", query(1, l, r)); } return 0; } ``` #### 动态开点线段树解法 ```cpp {.line-numbers} #include using namespace std; const int N = 1000010; int n, m, p; // 动态开点线段树 #define LL long long #define ls tr[u].l // u节点的左子节点号 #define rs tr[u].r // u节点的右儿子节点号 #define mid ((l + r) >> 1) struct Node { int l, r; // 注意:这里的[l,r]与普通线段树不同!!普通线段树中是控制的范围,动态开点线段树是左右儿子的节点号! // 动态开点线段树中u节点的控制范围是通过函数的2,3两个参数传递过去的,不是记录在tr[u].l,tr[u].r中的!记录在tr[u].l,tr[u].r中的是左右儿子的节点号! // 理解清楚这一点非常重要,因为后面在求区间长度是,普通线段树len(u)=(tr[u].r-tr[u].l+1),而动态开点线段树len(u)=(r-l+1),len(ls)=mid-l+1,len(rs)=r-mid int mu, add; // 乘法标记,加法标记 LL sum; // 区间和 } tr[N << 1]; int root, idx; // 汇总 void pushup(int u) { tr[u].sum = (tr[ls].sum + tr[rs].sum) % p; } // 创建节点:节点号分配,懒标记初始化 void build(int &u) { if (u) return; u = ++idx; tr[u].add = 0; tr[u].mu = 1; } void pushdown(int &u, int l, int r) { if (tr[u].add == 0 && tr[u].mu == 1) return; // 默认懒标记 build(ls), build(rs); // 左儿子创建, 右儿子创建 int &mu = tr[u].mu, &add = tr[u].add; // 此时的add懒标记,已经处理过了 tr[ls].sum = ((LL)mu * tr[ls].sum % p + (LL)(mid - l + 1) * add % p) % p; tr[rs].sum = ((LL)mu * tr[rs].sum % p + (LL)(r - mid) * add % p) % p; tr[ls].mu = (LL)tr[ls].mu * mu % p; tr[rs].mu = (LL)tr[rs].mu * mu % p; tr[ls].add = ((LL)tr[ls].add * mu % p + add) % p; tr[rs].add = ((LL)tr[rs].add * mu % p + add) % p; mu = 1, add = 0; // 清空懒标记 } // 乘法 void mu(int &u, int l, int r, int L, int R, int v) { build(u); // 动态开点 if (l >= L && r <= R) { // 如果区间被完整覆盖 tr[u].add = (LL)tr[u].add * v % p; // 比较重要的一步,add要在这里乘上v,因为后面可能要加其他的数而那些数其实是不用乘k的 tr[u].mu = (LL)tr[u].mu * v % p; tr[u].sum = (LL)tr[u].sum * v % p; return; } if (l > R || r < L) return; // 如果没有交集 // 下传懒标记 pushdown(u, l, r); // 分裂 mu(ls, l, mid, L, R, v), mu(rs, mid + 1, r, L, R, v); // 汇总 pushup(u); } // 加法 void add(int &u, int l, int r, int L, int R, int v) { build(u); // 动态开点 if (l >= L && r <= R) { // 如果区间被完整覆盖 tr[u].add = ((LL)tr[u].add + v) % p; tr[u].sum = ((LL)tr[u].sum + ((LL)v * ((LL)r - l + 1)) % p) % p; return; } if (l > R || r < L) return; // 如果没有交集 // 下传懒标记 pushdown(u, l, r); // 分裂 add(ls, l, mid, L, R, v), add(rs, mid + 1, r, L, R, v); // 汇总 pushup(u); } // 区间查询 LL query(int u, int l, int r, int L, int R) { if (l >= L && r <= R) return tr[u].sum; // 如果完整命中,返回我的全部 if (l > R || r < L) return 0; // 如果与我无关,返回0 pushdown(u, l, r); return (query(ls, l, mid, L, R) % p + query(rs, mid + 1, r, L, R) % p) % p; } int main() { // 文件输入输出 #ifndef ONLINE_JUDGE freopen("P3373_2.in", "r", stdin); #endif // 加快读入 ios::sync_with_stdio(false), cin.tie(0); cin >> n >> m >> p; for (int i = 1; i <= n; i++) { int x; cin >> x; add(root, 1, n, i, i, x); } while (m--) { int op, l, r; cin >> op >> l >> r; if (op == 1) { int k; cin >> k; mu(root, 1, n, l, r, k); } else if (op == 2) { int k; cin >> k; add(root, 1, n, l, r, k); } else printf("%lld\n", query(root, 1, n, l, r)); } return 0; } ```