Part.0 前置知识
- 树状数组
- 差分
Part.1 树状数组的区间修改
说实在的这东西线段树也可以弄,只是代码有亿点长而已
不妨记 a i a_i ai 为原数组的第 i i i个值,那么首先定义差分数组 d i = a i − a i − 1 d_i = a_i - a_{i - 1} di=ai−ai−1。
我们可以注意到这个差分数组的一些奇妙性质:
- a i = ∑ k = 1 i d k a_i = \sum_{k = 1}^{i} d_k ai=∑k=1idk
- 如果要在某区间 [ l , r ] [l, r] [l,r] 加、减去某值 v v v,那么我们只需在 l l l 处加(减) v v v,在 r + 1 r + 1 r+1 处减(加) v v v
因此对原数组进行区间修改就等价于在差分数组上进行两次操作,进行单点查询就等价求差分数组的前缀和。
Part.2 树状数组的区间查询
虽然在不考虑区间修改的区间查询用树状数组做简直太简单了,但是如果考虑了区间修改了呢?
显然我们必须借助差分数组来进行查询。
先推一下求 a i a_i ai 前缀和的式子:
∑ i = 1 k a i = ∑ i = 1 k ∑ j = 1 i d j = ∑ i = 1 k d i × ( k − i + 1 ) = ( k + 1 ) ⋅ ∑ i = 1 k d i − ∑ i = 1 k i ⋅ d i \begin{align*} \sum_{i = 1}^{k}{a_i} &= \sum_{i = 1}^{k} {\sum_{j = 1}^{i}{d_j}} \\ &= \sum_{i = 1}^{k}d_i \times (k - i + 1) \\ &= (k + 1) \cdot \sum_{i = 1}^{k} d_i - \sum_{i = 1}^{k} i \cdot d_i \end{align*} i=1∑kai=i=1∑kj=1∑idj=i=1∑kdi×(k−i+1)=(k+1)⋅i=1∑kdi−i=1∑ki⋅di
因而我们只需要维护两个树状数组,一个用来记录 d i d_i di,另一个用来记录 i ⋅ d i i \cdot d_i i⋅di 即可。
而查询时仅需两次查询作差即可。
Part.3 板题与其代码实现
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MaxN = (int)1e6;
int N;
ll A[MaxN + 5];
ll s1[MaxN + 5], s2[MaxN + 5];
inline int lowbit(int x) {return x & (-x);}
inline void modify(int pos, ll val) {
for(int i = pos; i <= N; i += lowbit(i))
s1[i] += val, s2[i] += pos * val;
}
inline void modify(int l, int r, ll val) {
modify(l, val), modify(r + 1, -val);
}
inline ll query(int x) {
ll ret = 0;
for(int i = x; i >= 1; i -= lowbit(i))
ret += (x + 1) * s1[i] - s2[i];
return ret;
}
inline ll query(int l, int r) {
return query(r) - query(l - 1);
}
int main() {
int M;
scanf("%d %d", &N, &M);
for(int i = 1; i <= N; i++)
scanf("%lld", &A[i]);
for(int i = 1; i <= N; i++)
modify(i, A[i] - A[i - 1]);
for(int i = 1; i <= M; i++) {
int opt;
scanf("%d", &opt);
if(opt == 1) {
int l, r, val;
scanf("%d %d %d", &l, &r, &val);
modify(l, r, val);
}
if(opt == 2) {
int l, r;
scanf("%d %d", &l, &r);
printf("%lld\n", query(l, r));
}
}
return 0;
}