##[$AcWing$ $243$. 一个简单的整数问题2](https://www.acwing.com/problem/content/244/) ### 一、题目描述 给定一个长度为 $N$ 的数列 $A$,以及 $M$条指令,每条指令可能是以下两种之一: `C l r d`,表示把 $A[l],A[l+1],…,A[r]$ 都加上 $d$。 `Q l r`,表示询问数列中第 $l∼r$个数的和。 对于每个询问,输出一个整数表示答案。 **输入格式** 第一行两个整数 $N,M$。 第二行 $N$个整数 $A[i]$。 接下来 $M$ 行表示 $M$条指令,每条指令的格式如题目描述所示。 **输出格式** 对于每个询问,输出一个整数表示答案。 每个答案占一行。 **数据范围** $1≤N,M≤105$,$|d|≤10000,|A[i]|≤10^9$ ### 二、树状数组
| 树状数组解决的问题 | 操作对象 | 例题 | | ------------------------ | ------------ | -------------------------------------------------------- | | **单点修改,区间查询** | 前缀和 | [点我](https://www.cnblogs.com/littlehb/p/16140758.html) | | **区间修改,单点查询** | 一个差分数组 | [点我](https://www.cnblogs.com/littlehb/p/16141314.html) | | **区间修改,区间和查询** | 两个差分数组 | **本题** |
#### 区间修改,单点查询 如果是区间修改,单点查询。只需用树状数组维护一个差分数组$b$,假设查询位置$x$,那么$\displaystyle \sum_{i=1}^{x}b_i$就是$x$位置上的变化后的值。 #### 区间修改+区间和查询 考虑引入区间查询。首先最暴力想,假设查询$[1,r]$。那么$[1,r]$的答案=$\displaystyle \sum_{i=1}^{r}\sum_{j=1}^{i}b_j$ 不妨举个特例,更直观些。假设查询$[1, 4]$。那么$ans=(b_1)+(b_1+b_2)+(b_1+b_2+b_3)+(b_1+b_2+b_3+b_4)=4b_1+3b_2+2b_3+1b_4$。 换成查询$[1, r]$。那么 $$\large ans = r*b_1+(r-1)*b_2+(r-3)*b_3+...+1*b_r \\=(r+1-1)b_1+(r+1-2)b_2+(r+1-3)b_3+…+(r+1-r)b_r \\= (r+1)\sum_{i=1}^{r}b_i-\sum_{i=1}^{r}i*b_i$$ ### 三、树状数组 ```cpp {.line-numbers} #include using namespace std; const int N = 1000010; // 树状数组 typedef long long LL; #define lowbit(x) (x & -x) LL c1[N], c2[N]; void add(LL x, LL d) { for (LL i = x; i < N; i += lowbit(i)) c1[i] += d, c2[i] += x * d; } LL sum(LL x) { LL res = 0; for (LL i = x; i; i -= lowbit(i)) res += (x + 1) * c1[i] - c2[i]; return res; } int n, m; LL a[N]; int main() { // 加快读入 ios::sync_with_stdio(false), cin.tie(0); #ifndef ONLINE_JUDGE freopen("243.in", "r", stdin); #endif cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; add(i, a[i] - a[i - 1]); // 保存基底是差分数组的树状数组 } while (m--) { string op; int x, y, d; cin >> op; if (op[0] == 'Q') { cin >> x >> y; printf("%lld\n", sum(y) - sum(x - 1)); } else { cin >> x >> y >> d; add(x, d), add(y + 1, -d); // 维护差分 } } return 0; } ``` #### 代码细节解读 ```cpp {.line-numbers} void add(LL x, LL v) { for (LL i = x; i < N; i += lowbit(i)) c1[i] += v, c2[i] += x * v; } LL sum(LL x) { LL res = 0; for (LL i = x; i; i -= lowbit(i)) res += (x + 1) * c1[i] - c2[i]; return res; } ``` #### 概念说明 - ① $b[i]=a[i]-a[i-1]$ : 原数组的差分数组 - ② $c_1[]$:**差分数组$b[]$对应的, 用于快速同步修改,快速统计分析 用的 树状数组**,利用树状数组前缀和的特性,快速获取到区间修改后的单点值。 - ③ $c_2[]$维护的是$i * b[i]$的前缀和 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308171405886.png) #### 修改流程 当我们给$a[i] \sim a[j]$加上$d$时,对应$c_1[]$就是差分的 **双点** 修改: ```cpp {.line-numbers} add(x,d),add(y+1,-d); ``` 对应的内部逻辑就是辅助数组$c_2[]$,它是差分数组$i*b[i]$的前缀和,原数组修改一下,它就跟着修改一下: ```cpp {.line-numbers} for (LL i = x; i < N; i += lowbit(i)) c1[i] += v, c2[i] += x * v; ``` 而在查询时,按推出的公式查询即可 ```cpp {.line-numbers} for (LL i = x; i; i -= lowbit(i)) res += (x + 1) * c1[i] - c2[i]; ``` ### 四、线段树+区间修改+懒标记+区间和查询 ```cpp {.line-numbers} #include using namespace std; typedef long long LL; const int N = 100010; int n, m; int a[N]; struct Node { int l, r; LL sum, tag; // 区间总和,修改的数值(懒标记) } tr[N << 2]; // 向祖先节点更新统计信息 void pushup(int u) { tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; // 向父节点更新sum和 } // 父节点向子节点传递懒标记 void pushdown(int u) { auto &root = tr[u], &ls = tr[u << 1], &rs = tr[u << 1 | 1]; if (root.tag) { // 如果存在懒标记 // tag传递到子段,子段的sum和需要按 区间长度*root.tag 进行增加 ls.tag += root.tag, ls.sum += (LL)(ls.r - ls.l + 1) * root.tag; rs.tag += root.tag, rs.sum += (LL)(rs.r - rs.l + 1) * root.tag; // 清除懒标记 root.tag = 0; } } // 构建 void build(int u, int l, int r) { tr[u] = {l, r}; // 标记范围 if (l == r) { // 叶子 tr[u] = {l, r, a[l], 0}; // 区间内只有一个元素l(r),区间和为a[l],不需要记录向下的传递tag return; } int mid = (l + r) >> 1; build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 左右儿子构建 pushup(u); // 通过左右儿子构建后,向祖先节点反馈统计信息变化 } // 以u为根,在区间[l,r]之间全都增加v void modify(int u, int l, int r, int v) { if (tr[u].l >= l && tr[u].r <= r) { // 如果区间完整命中 tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * v; // 总和增加 = 区间长度*v tr[u].tag += v; // 懒标记+v return; } pushdown(u); // 如果自己身上有旧的tag数值,在递归前需要将原tag值pushdown到子孙节点去 int mid = (tr[u].l + tr[u].r) >> 1; if (l <= mid) modify(u << 1, l, r, v); // 与左区间有交集 if (r > mid) modify(u << 1 | 1, l, r, v); // 与右区间有交集 pushup(u); // 将结果的变更更新到祖先节点 } // 关键的查询操作 LL query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum; // 记住原则:有懒标记的区间修改,都是先pushdown消除掉懒标记,再分裂 pushdown(u); int mid = tr[u].l + tr[u].r >> 1; LL sum = 0; if (l <= mid) sum = query(u << 1, l, r); if (r > mid) sum += query(u << 1 | 1, l, r); // 左查+ 右查 = 总和 return sum; } int main() { // 加快读入 ios::sync_with_stdio(false), cin.tie(0); cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; // 构建线段树 build(1, 1, n); char op; int l, r, d; while (m--) { cin >> op >> l >> r; if (op == 'C') { cin >> d; modify(1, l, r, d); // 区间修改 } else printf("%lld\n", query(1, l, r)); // 区间查询 } return 0; } ```