题目地址:
https://www.acwing.com/problem/content/244/
给定一个长度为
N
N
N的数列
A
A
A,以及
M
M
M条指令,每条指令可能是以下两种之一:C l r d
,表示把
A
[
l
]
,
A
[
l
+
1
]
,
…
,
A
[
r
]
A[l],A[l+1],…,A[r]
A[l],A[l+1],…,A[r]都加上
d
d
d;Q l r
,表示询问数列中第
l
∼
r
l∼r
l∼r个数的和。对于每个询问,输出一个整数表示答案。下标从
1
1
1开始。
输入格式:
第一行两个整数
N
,
M
N,M
N,M。第二行
N
N
N个整数
A
[
i
]
A[i]
A[i]。接下来
M
M
M行表示
M
M
M条指令,每条指令的格式如题目描述所示。
输出格式:
对于每个询问,输出一个整数表示答案。每个答案占一行。
数据范围:
1
≤
N
,
M
≤
1
0
5
1≤N,M≤10^5
1≤N,M≤105
∣
d
∣
≤
10000
|d|≤10000
∣d∣≤10000
∣
A
[
i
]
∣
≤
1
0
9
|A[i]|≤10^9
∣A[i]∣≤109
法1:树状数组。考虑维护 A A A的差分数组 d d d,将 A [ l : r ] A[l:r] A[l:r]都加上 x x x等价于将 d [ l ] d[l] d[l]加上 x x x,然后将 d [ r + 1 ] d[r+1] d[r+1]减去 x x x。求 A [ l : r ] A[l:r] A[l:r]的和,可以推公式: ∑ A [ l : r ] = ∑ k = l r A [ k ] = ∑ k = l r ∑ i = 1 k d [ i ] = ∑ k = 1 r ∑ i = 1 k d [ i ] − ∑ k = 1 l − 1 ∑ i = 1 k d [ i ] \sum A[l:r]=\sum_{k=l}^{r}A[k]=\sum_{k=l}^{r}\sum_{i=1}^{k} d[i]\\=\sum_{k=1}^{r}\sum_{i=1}^{k} d[i]-\sum_{k=1}^{l-1}\sum_{i=1}^{k} d[i] ∑A[l:r]=k=l∑rA[k]=k=l∑ri=1∑kd[i]=k=1∑ri=1∑kd[i]−k=1∑l−1i=1∑kd[i]考虑 ∑ k = 1 s ∑ i = 1 k d [ i ] \sum_{k=1}^{s}\sum_{i=1}^{k} d[i] ∑k=1s∑i=1kd[i],有 ∑ k = 1 s ∑ i = 1 k d [ i ] = s ∑ i = 1 s d [ i ] − ∑ i = 1 s ( i − 1 ) d [ i ] \sum_{k=1}^{s}\sum_{i=1}^{k} d[i]=s\sum_{i=1}^{s}d[i]-\sum_{i=1}^{s}(i-1)d[i] k=1∑si=1∑kd[i]=si=1∑sd[i]−i=1∑s(i−1)d[i]所以我们可以开两个树状数组 t 1 t_1 t1和 t 2 t_2 t2,分别维护 d [ i ] d[i] d[i]数组和 ( i − 1 ) d [ i ] (i-1)d[i] (i−1)d[i]数组,这样一来,将 A [ l : r ] A[l:r] A[l:r]都加上 x x x等价于将 t 1 [ l ] t_1[l] t1[l]加上 x x x,将 t 1 [ r + 1 ] t_1[r+1] t1[r+1]减去 x x x,并且将 t 2 [ l ] t_2[l] t2[l]加上 ( l − 1 ) x (l-1)x (l−1)x,将 t 2 [ r ] t_2[r] t2[r]减去 r x rx rx;求 A [ 1 : s ] A[1:s] A[1:s]的和等价于求 s s s乘以 t 1 t_1 t1维护的数组的前 s s s个数的和再减去 t 2 t_2 t2维护的数组的前 s s s个数的和。代码如下:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m, w[N];
long tr1[N], tr2[N];
int lowbit(int x) {
return x & -x;
}
void add(int k, long x, long tr[]) {
for (int i = k; i <= n; i += lowbit(i)) tr[i] += x;
}
long sum(int k, long tr[]) {
long res = 0;
for (int i = k; i; i -= lowbit(i)) res += tr[i];
return res;
}
// 返回A数组的前k个数的和
long pre_sum(int k) {
return sum(k, tr1) * k - sum(k, tr2);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
for (int i = 1; i <= n; i++) {
add(i, w[i] - w[i - 1], tr1);
add(i, (long) (i - 1) * (w[i] - w[i - 1]), tr2);
}
while (m--) {
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if (op[0] == 'C') {
scanf("%d", &d);
add(l, d, tr1), add(l, (l - 1) * d, tr2);
add(r + 1, -d, tr1), add(r + 1, r * (-d), tr2);
} else printf("%ld\n", pre_sum(r) - pre_sum(l - 1));
}
return 0;
}
预处理时间复杂度 O ( N log N ) O(N\log N) O(NlogN),每次操作时间 O ( log N ) O(\log N) O(logN),空间 O ( N ) O(N) O(N)。
法2:线段树。一般的线段树可以处理单点修改,只需要做“pushup操作”即可,每个节点存一个值 s s s,表示这个节点维护的区间的和。这里还需要区间修改,可以用懒标记来做,这个懒标记是这样做的,当要修改的区间完全覆盖当前区间的时候,我们就不递归下去了,而将这整个区间做个标记(比如每个树节点开个新变量叫 a a a,这个 a a a专门存当前区间需要增加但是还没传导到孩子节点的数,比如说某次要将 [ 1 , 4 ] [1,4] [1,4]这个区间加上数 x x x,那么当递归到 [ 1 , 3 ] [1,3] [1,3]这个区间的时候,就可以停止向下递归,而将这个区间的 a a a值增加 x x x),这样做的好处是将区间修改的复杂度也降到 O ( log N ) O(\log N) O(logN)的级别,而不需要让每个单点都得到修改;而只要 [ 1 , 3 ] [1,3] [1,3]这个区间完全含在询问区间 [ l , r ] [l,r] [l,r]里的时候,就可以不去管这个区间的孩子的 s s s值是否正确,此时 a a a值完好不动也没有关系;如果询问区间只是与 [ 1 , 3 ] [1,3] [1,3]有交集而不完全包含,此时就需要将懒标记的值传导到它的孩子里去,让其两个孩子都根据 a a a值做出修改,修改完后,将 a a a清零,接下来继续递归询问,询问到多深,懒标记就传导到多深。传导懒标记的过程就叫做“pushdown操作”。代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int w[N];
struct Node {
int l, r;
long sum, add;
} tr[4 * N];
// 让孩子来更新tr[u]
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
// 将u的懒标记传导给tr[u]的两个孩子
void pushdown(int u) {
auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (root.add) {
left.add += root.add, left.sum += (long) (left.r - left.l + 1) * root.add;
right.add += root.add, right.sum += (long) (right.r - right.l + 1) * root.add;
// 传导完了之后标记值要清零,表示传导完毕了
root.add = 0;
}
}
void build(int u, int l, int r) {
if (l == r) tr[u] = {l, r, w[l], 0};
else {
tr[u] = {l, r};
int mid = l + (r - l >> 1);
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void modify(int u, int l, int r, int d) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].sum += (long) (tr[u].r - tr[u].l + 1) * d;
// 完全包含tr[u]所维护的区间的时候,就可以做懒标记了,而不继续传导下去
tr[u].add += d;
} else {
// 否则需要先传导给下去
pushdown(u);
int mid = tr[u].l + (tr[u].r - tr[u].l >> 1);
if (l <= mid) modify(u << 1, l, r, d);
if (r > mid) modify(u << 1 | 1, l, r, d);
pushup(u);
}
}
long query(int u, int l, int r) {
// 如果询问的区间完全包含tr[u]所维护的区间,则直接返回sum值
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
// 否则说明要询问孩子,那么先将懒标记传导给孩子,将孩子更新成正确值,然后继续向下询问
pushdown(u);
int mid = tr[u].l + (tr[u].r - tr[u].l >> 1);
long 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() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
build(1, 1, n);
while (m--) {
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if (op[0] == 'C') {
cin >> d;
modify(1, l, r, d);
} else printf("%ld\n", query(1, l, r));
}
return 0;
}
预处理时间复杂度 O ( N ) O(N) O(N),每次询问时间 O ( log N ) O(\log N) O(logN),空间 O ( N ) O(N) O(N)。
法2:分块。思想是将这个数组分成若干尽量均匀的块(最后一个块可能是不均匀的,但不会影响算法正确性),然后利用懒标记思想加速修改和查询操作。一般是分成
N
\sqrt N
N块,这样每块的长度尽量是
N
\sqrt N
N。然后再维护两个数组
a
a
a和
s
s
s,使得
s
[
i
]
s[i]
s[i]是第
i
i
i块的数字和,而
a
[
i
]
a[i]
a[i]是第
i
i
i块的懒标记(这里的下标是从
0
0
0开始的。
a
[
i
]
a[i]
a[i]表示第
i
i
i个块里的数字需要加上
a
[
i
]
a[i]
a[i],但是这个操作并没有真的加到数组
A
A
A上去,而只是标记了一下。正是标记这个操作能使得操作加速)。设每个块的大小是
l
l
l,那么第
i
i
i个数所在的块是第
i
/
l
i/l
i/l个(这样做的话第
0
0
0个块事实上只有
l
−
1
l-1
l−1个数,但这其实是无所谓的,分块依然比较均匀,也不影响复杂度)。初始化的时候,需要读入数组
A
A
A,并初始化数组
s
s
s。两个操作如下进行:
1、查询
[
l
,
r
]
[l,r]
[l,r]的数字和,先看一下这个区间是否在一个整块里,如果是,则直接暴力求解;如果不是,那么它一定横跨了至少两个块,则先将最左端和最右端位于不完整块里的数字暴力求和并注意加上懒标记,因为懒标记是没有真的加到数组
A
A
A上的,接着加上中间完整块的和。由于每个块的大小最多是
N
\sqrt N
N,并且总共最多有
O
(
N
)
O(\sqrt N)
O(N)个块,所以查询的时间复杂度是
O
(
N
)
O(\sqrt N)
O(N)。
2、将
[
l
,
r
]
[l,r]
[l,r]这个区间所有数都加上
d
d
d,和查询类似,先看一下区间是否在一个整块里,如果是,则暴力修改数组
A
A
A,并且对应地修改数组
s
s
s;如果不是,则先将最左端和最右端位于不完整块里的数字暴力修改(也是修改
A
A
A和
s
s
s两个数组),中间的完整的块只需修改
s
s
s和懒标记数组
a
a
a,修改
s
s
s的时候只需要将
s
[
i
]
s[i]
s[i]加上
l
d
ld
ld就行了,而对于懒标记数组,只需要将
a
[
i
]
a[i]
a[i]加上
d
d
d。时间复杂度也是
O
(
N
)
O(\sqrt N)
O(N)。
代码如下:
#include <iostream>
#include <cmath>
using namespace std;
const int N = 100010, M = 350;
int n, m, len;
long add[M], sum[M];
int w[N];
int bel[N];
void change(int l, int r, int d) {
if (bel[l] == bel[r]) for (int i = l; i <= r; i++) w[i] += d, sum[bel[i]] += d;
else {
int i = l, j = r;
while (bel[i] == bel[l]) w[i++] += d, sum[bel[l]] += d;
while (bel[j] == bel[r]) w[j--] += d, sum[bel[r]] += d;
for (int k = bel[i]; k <= bel[j]; k++) sum[k] += len * d, add[k] += d;
}
}
long query(int l, int r) {
long res = 0;
if (bel[l] == bel[r]) for (int i = l; i <= r; i++) res += w[i] + add[bel[i]];
else {
int i = l, j = r;
while (bel[i] == bel[l]) res += w[i++] + add[bel[l]];
while (bel[j] == bel[r]) res += w[j--] + add[bel[r]];
for (int k = bel[i]; k <= bel[j]; k++) res += sum[k];
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
len = sqrt(n);
for (int i = 1; i <= n; i++) {
scanf("%d", &w[i]);
bel[i] = i / len;
sum[bel[i]] += w[i];
}
char op[2];
int l, r, d;
while (m--) {
scanf("%s%d%d", op, &l, &r);
if (op[0] == 'C') {
scanf("%d", &d);
change(l, r, d);
} else printf("%ld\n", query(l, r));
}
return 0;
}
预处理时间复杂度
O
(
N
)
O(N)
O(N),每次操作
O
(
N
)
O(\sqrt N)
O(N),空间
O
(
N
)
O(N)
O(N)(这个bel
数组是可以起到加速效果的,否则每次计算
i
i
i在哪个块里都需要做除法,时间消耗比较大)。