前缀和
前置知识:
∑
i
=
1
r
a
i
=
a
l
+
a
l
+
1
+
⋯
+
a
r
−
1
+
a
r
\sum_{i = 1}^{r}{a_i} = a_l + a_{l + 1} + \dots + a_{r - 1} + a_r
∑i=1rai=al+al+1+⋯+ar−1+ar
考虑以下数组:
i i i | 1 1 1 | 2 2 2 | 3 3 3 |
---|---|---|---|
a I a_I aI | 3 3 3 | 5 5 5 | 7 7 7 |
如果我们要查询
∑
i
=
1
2
a
i
\sum_{i = 1}^{2}{a_i}
∑i=12ai,很显然可以得到
∑
i
=
1
2
a
i
=
3
+
5
=
8
\sum_{i = 1}^{2}{a_i} = 3 + 5 = 8
∑i=12ai=3+5=8。如果我们要查询
∑
i
=
l
r
a
i
\sum_{i = l}^{r}{a_i}
∑i=lrai,则每次的时间复杂度为
O
(
r
−
l
)
O(r -l)
O(r−l)。
如果我们对于一个长度为
n
n
n 的数组查询
k
k
k 次(不修改),则时间复杂度为
O
(
n
k
)
O(nk)
O(nk) 。如果给定一个
n
=
1
0
7
,
k
=
1
0
6
n = 10^7,k = 10^6
n=107,k=106,我们要如何让程序在 1s 内运行完呢?
i i i | 1 1 1 | 2 2 2 | 3 3 3 |
---|---|---|---|
a I a_I aI | 3 3 3 | 5 5 5 | 7 7 7 |
b I b_I bI | 3 3 3 | 8 8 8 | 15 15 15 |
我们构造 b 0 = 0 , b i = b i − 1 + a i b_0 = 0,b_i = b_{i - 1} + a_i b0=0,bi=bi−1+ai,同上表,倘若我们只考虑快速查询 ∑ i = 1 r a i \sum_{i = 1}^{r}{a_i} ∑i=1rai,显而易见原式即为 ∑ i = 1 r a i = b i \sum_{i = 1}^{r}{a_i} = b_i ∑i=1rai=bi,而 b b b 数组可以在 O ( n ) O(n) O(n) 的时间范围内预处理完。简易证明见下:
b i = b i − 1 + a i = b i − 2 + a i − 1 + a i = ∑ i = 1 r a i b_i = b_{i - 1} + a_i = b_{i - 2} + a_{i - 1} + a_i = \sum_{i = 1}^{r}{a_i} bi=bi−1+ai=bi−2+ai−1+ai=∑i=1rai
假如我们转换一下思路,可以发现
∑
i
=
l
r
a
i
=
∑
i
=
1
r
a
i
−
∑
i
=
1
l
−
1
a
i
=
b
r
−
b
l
−
1
\sum_{i = l}^{r}{a_i} = \sum_{i = 1}^{r}{a_i} - \sum_{i = 1}^{l-1}{a_i} = b_r - b_{l - 1}
∑i=lrai=∑i=1rai−∑i=1l−1ai=br−bl−1,那么我们将单次查询复杂度降到了
O
(
1
)
O(1)
O(1),总时间复杂度变为
O
(
n
+
k
)
O(n + k)
O(n+k)。
例题
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
// 快读
inline int read() {
int x = 0,f = 1;char ch = getchar();
while (ch < '0' or ch > '9'){if (ch == '-') f = -1;ch = getchar();}
while (ch >= '0' and ch <='9'){x = x * 10 + ch - 48;ch = getchar();}
return x * f;
}
//快写
inline void write(int x) {
if(x < 0) putchar('-'),x = -x;
if(x > 9) write(x / 10);
putchar(x % 10 + '0');
return;
}
//输出char
void writec(char x) {
putchar(x);
return;
}
int main() {
int n = read(),a[100005],m;
for(int i = 1;i <= n;i++) a[i] = read() + a[i - 1];
m = read();
while(m--) {
int l = read() - 1,r = read();
write(a[r] - a[l]),writec('\n');
}
return 0;
}
差分
考虑完不修改多次查询,如果我们多次将区间内的所有数加上一个值,最后统一查询,那么会怎么样呢?
假设我们操作2次,第一次让
a
1
…
a
3
←
(
a
1
+
3
)
…
(
a
3
+
3
)
a_1 \dots a_3 \gets(a_1 + 3)\dots (a_3+3)
a1…a3←(a1+3)…(a3+3),第二次让
a
1
,
a
2
←
(
a
1
+
2
)
…
(
a
2
+
2
)
a_1 ,a_2 \gets(a_1 + 2)\dots (a_2+2)
a1,a2←(a1+2)…(a2+2)
i i i | 1 1 1 | 2 2 2 | 3 3 3 |
---|---|---|---|
原数组 | 3 3 3 | 5 5 5 | 7 7 7 |
操作一次 | 6 6 6 | 8 8 8 | 10 10 10 |
操作两次 | 8 8 8 | 10 10 10 | 10 10 10 |
如果你对这一些规律不太敏感的话,可以考虑 b i = a i − a i − 1 b_i = a_i - a_{i - 1} bi=ai−ai−1,那么:
i i i | 1 1 1 | 2 2 2 | 3 3 3 |
---|---|---|---|
原 b i b_i bi数组 | 3 3 3 | 2 2 2 | 2 2 2 |
操作一次 | 6 6 6 | 2 2 2 | 2 2 2 |
操作两次 | 8 8 8 | 2 2 2 | 0 0 0 |
显而易见,如果区间加减, b i b_i bi 只会改变两个值,切规律如下:
b l ← b l + k , b r + 1 ← b r + 1 − k b_l \gets b_l + k,b_{r + 1} \gets b_{r + 1}- k bl←bl+k,br+1←br+1−k。
我们将每次修改由 O ( n ) O(n) O(n) 变成了 O ( 1 ) O(1) O(1)。
代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<queue>
#include<map>
using namespace std;
int x[10005];
// 快读
inline int read()
{
int x = 0,f = 1;char ch = getchar();
while (ch < '0' or ch > '9'){if (ch == '-') f = -1;ch = getchar();}
while (ch >= '0' and ch<='9'){x = x * 10 + ch - 48;ch = getchar();}
return x * f;
}
//快写
inline void write(int x) {
if(x < 0) putchar('-'),x = -x;
if(x > 9) write(x / 10);
putchar(x % 10 + '0');
return;
}
//输出char
void writec(char x) {
putchar(x);
return;
}
int main() {
int t = read();
while(t--) {
int n = read(),u = read();
for(int i = 0;i < n;i++) x[i] = 0;
while(u--) {
int l = read(),r = read(),val = read();
x[l] += val;
x[r + 1] -= val;
}
for(int i = 1;i < n;i++) x[i] += x[i - 1];
int q = read();
while(q--) {
int i = read();
write(x[i]),writec('\n');
}
}
return 0;
}