考虑这样一个题目:
给定一个长度为N的数组,有Q个询问,每次询问这个数组中一段连续区间的和。
实例:
[
4
,
5
,
6
,
−
2
,
3
,
10
]
[4, 5, 6, -2, 3, 10]
[4,5,6,−2,3,10] 询问第2到第5个数字之和等于12。
思考:
如果按照题目描述进行模拟,那么每个询问都需要使用询问进行求和,速度太慢。
我们可以使用前缀和进行加速。
一维前缀和:
我们假设输入的数组名为 d a t a data data 。
定义前缀和数组 s u m sum sum,前缀和数组中的每个元素 s u m [ i ] sum[i] sum[i]等于 d a t a data data数组前 i i i个元素之和,即 s u m [ i ] = d a t a [ 1 ] + d a t a [ 2 ] + ⋯ + d a t a [ i ] sum[i]=data[1]+data[2]+\cdots+data[i] sum[i]=data[1]+data[2]+⋯+data[i] 。
可以得到一个更快的递推式: s u m [ i ] = s u m [ i − 1 ] + d a t a [ i ] sum[i]=sum[i-1]+data[i] sum[i]=sum[i−1]+data[i],其中 s u m [ 0 ] = 0 sum[0]=0 sum[0]=0,因此可以在 O ( N ) O(N) O(N)的时间复杂度内计算出前缀和数组。
对于原问题中的一个询问,假设需要计算第
a
a
a个数到第
b
b
b个数之和,那么有:
d
a
t
a
[
a
]
+
d
a
t
a
[
a
+
1
]
+
⋯
+
d
a
t
a
[
b
]
=
(
d
a
t
a
[
1
]
+
d
a
t
a
[
2
]
+
⋯
+
d
a
t
a
[
b
]
)
−
(
d
a
t
a
[
1
]
+
d
a
t
a
[
2
]
+
⋯
+
d
a
t
a
[
a
−
1
]
)
=
s
u
m
[
b
]
−
s
u
m
[
a
−
1
]
data[a]+data[a+1]+\cdots+data[b] =(data[1]+data[2]+\cdots+data[b])-(data[1]+data[2]+\cdots+data[a-1]) =sum[b]-sum[a-1]
data[a]+data[a+1]+⋯+data[b]=(data[1]+data[2]+⋯+data[b])−(data[1]+data[2]+⋯+data[a−1])=sum[b]−sum[a−1]
可以发现,上面的式子在 a = 1 a=1 a=1等边界情况也是成立的。
因此原数组中一段区间的和等于前缀和数组中两个数的差,用这种方式我们可以 O ( 1 ) O(1) O(1)计算出每个询问的答案。
模版题
C++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
s[i] = s[i - 1] + a[i];// 前缀和的初始化
while (m--)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]);// 区间和的计算
}
return 0;
}
考虑扩展题目:
给定一个大小为
N
×
M
N \times M
N×M的整数矩阵,有
Q
Q
Q个询问,每次询问这个矩阵的一个子矩阵的和。
示例(假设行坐标往下增长,列坐标往右增长):
假设某个询问上图中涂色的子矩阵,那么答案为41 。
二维前缀和:
二维前缀和即是将一维前缀和扩展到了二维的情况。
依然假设上图中输入的矩阵名为
d
a
t
a
data
data 。
定义二维前缀和
s
u
m
sum
sum,其中
s
u
m
[
i
]
[
j
]
sum[i][j]
sum[i][j]为
d
a
t
a
data
data矩阵左上角的部分和,如下图:
上图中蓝色部分就是
s
u
m
[
3
]
[
3
]
sum[3][3]
sum[3][3]所包含的
d
a
t
a
data
data之和。
可以得到递推式:
s
u
m
[
i
]
[
j
]
=
s
u
m
[
i
−
1
]
[
j
]
+
s
u
m
[
i
]
[
j
−
1
]
−
s
u
m
[
i
−
1
]
[
j
−
1
]
+
d
a
t
a
[
i
]
[
j
]
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+data[i][j]
sum[i][j]=sum[i−1][j]+sum[i][j−1]−sum[i−1][j−1]+data[i][j],其原理如下图:
对于超出矩阵的部分和
s
u
m
[
i
]
[
j
]
sum[i][j]
sum[i][j]均设置为
0
0
0 。
那么,对于计算一个子矩阵的和(假设子矩阵的左上角坐标为
(
a
,
b
)
(a,b)
(a,b),右下角的坐标为
(
c
,
d
)
(c,d)
(c,d)),使用二维前缀和可直接计算得
s
u
m
[
c
]
[
d
]
−
s
u
m
[
c
]
[
b
−
1
]
−
s
u
m
[
a
−
1
]
[
d
]
+
s
u
m
[
a
−
1
]
[
b
−
1
]
sum[c][d]-sum[c][b-1]-sum[a-1][d]+sum[a-1][b-1]
sum[c][d]−sum[c][b−1]−sum[a−1][d]+sum[a−1][b−1],
其原理如下图:
模版题
C++代码
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], s[N][N];
int main()
{
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];// 求前缀和
while (q--)
{
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);// 算子矩阵的和
}
return 0;
}