AcWing 算法基础课笔记【基础算法篇】4.前缀和与差分
一维前缀和
思想
假设有一个数组
A
:
A
1
,
A
2
,
.
.
.
,
A
n
A:A_{1},A_{2},...,A_{n}
A:A1,A2,...,An
那么可以定义一个前缀和数组
S
:
S
i
=
A
1
+
A
2
+
.
.
.
+
A
i
S:S_{i}=A_{1}+A{2}+...+A_{i}
S:Si=A1+A2+...+Ai
其中,
S
0
=
0
S_{0}=0
S0=0
作用
求原数组中一段数的和
若不使用前缀和数组,则必须一位一位的加,时间复杂度为
O
(
n
)
O(n)
O(n)
但是使用前缀和数组,预处理的时间复杂度为
O
(
n
)
O(n)
O(n),之后每次计算的时间复杂度为
O
(
1
)
O(1)
O(1),即
A
l
+
.
.
.
+
A
r
=
S
r
−
S
l
−
1
A_{l}+...+A_{r}=S_{r}-S_{l-1}
Al+...+Ar=Sr−Sl−1
递归求
S
i
S_{i}
Si:
S
i
=
S
i
−
1
+
a
i
S_{i}=S_{i-1}+a_{i}
Si=Si−1+ai
代码
const int N = 100010;
int a[N], s[N];
int main(){
int n, m;
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;
}
二维前缀和
思想
S
i
,
j
S_{i,j}
Si,j表示矩阵中以
(
0
,
0
)
、
(
i
,
j
)
(0,0)、(i,j)
(0,0)、(i,j)为边界点的子矩阵中所有元素的和
假设要求以
(
x
1
,
y
1
)
、
(
x
2
,
y
2
)
(x_{1},y_{1})、(x_{2},y_{2})
(x1,y1)、(x2,y2)分别为左上角、右下角坐标的子矩阵中的元素和
则等于
S
x
2
,
y
2
−
S
x
2
,
y
1
−
1
−
S
x
1
−
1
,
y
2
+
S
x
1
−
1
,
y
1
−
1
S_{x_{2},y_{2}}-S_{x_{2},y_{1}-1}-S_{x_{1}-1,y_{2}}+S_{x_{1}-1,y_{1}-1}
Sx2,y2−Sx2,y1−1−Sx1−1,y2+Sx1−1,y1−1
如何递推求
S
i
,
j
S_{i,j}
Si,j?
S
i
,
j
=
S
i
−
1
,
j
+
S
i
,
j
−
1
−
S
i
−
1
,
j
−
1
+
a
i
,
j
S_{i,j}=S_{i-1,j}+S_{i,j-1}-S_{i-1,j-1}+a_{i,j}
Si,j=Si−1,j+Si,j−1−Si−1,j−1+ai,j
代码
int n, m, q;
const int N = 1010;
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;
}
一维差分
思想
假设有一个数组a,构造一个数组b,使得数组a是数组b的前缀和数组
此时,b为a的差分数组
作用
如果要多次将原数组a的
[
l
,
r
]
[l,r]
[l,r]区间内的所有元素加上
c
c
c,时间复杂度为
O
(
次数
∗
n
)
O(次数*n)
O(次数∗n)
此时,可以将
b
l
+
c
、
b
r
+
1
−
c
b_{l}+c、b_{r+1}-c
bl+c、br+1−c,最后再根据b数组推出原数组a,时间复杂度为
O
(
n
+
次数
)
O(n+次数)
O(n+次数)
如何构造数组b:
将差分数组初始都设为0,可以看成在
[
i
,
i
]
[i,i]
[i,i]范围内加上
a
i
a_{i}
ai
代码
const int N = 100010;
int a[N], b[N];
int n, m;
void insert(int l, int r, int c){
b[l] += c;
b[r + 1] -= c;
}
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++) insert(i, i, a[i]); // 构造差分数组b
while(m--){ // 处理数组
int l, r, c;
scanf("%d %d %d", &l, &r, &c);
insert(l, r, c);
}
for(int i = 1; i <= n; i++) a[i] = a[i - 1] + b[i]; // 递归求数组a
for(int i = 1; i <= n; i++) printf("%d ", a[i]);
return 0;
}
二维差分
作用
b
i
,
j
+
=
c
b_{i,j}+=c
bi,j+=c表示在原矩阵a中,以点
(
i
,
j
)
(i,j)
(i,j)作为左上角,以整个矩阵的右下角作为右下角,这个子矩阵中的所有元素加上c
假设要操作的矩阵,左上角坐标为
(
x
1
,
y
1
)
(x_{1},y_{1})
(x1,y1),右下角坐标为
(
x
2
,
y
2
)
(x_{2},y_{2})
(x2,y2),将这个范围内的子矩阵加上c:
b
x
1
,
y
1
+
=
c
,
b
x
2
+
1
,
y
1
−
=
c
,
b
x
1
,
y
2
+
1
−
=
c
,
b
x
2
+
1
,
y
2
+
1
+
=
c
b_{x_{1},y_{1}}+=c,b_{x_{2}+1,y_{1}}-=c,b_{x_{1},y_{2}+1}-=c,b_{x_{2}+1,y_{2}+1}+=c
bx1,y1+=c,bx2+1,y1−=c,bx1,y2+1−=c,bx2+1,y2+1+=c
递归求
a
i
,
j
a_{i,j}
ai,j:
a
i
,
j
=
a
i
−
1
,
j
+
a
i
,
j
−
1
−
a
i
−
1
,
j
−
1
+
b
i
,
j
a_{i,j}=a_{i-1,j}+a_{i,j-1}-a_{i-1,j-1}+b_{i,j}
ai,j=ai−1,j+ai,j−1−ai−1,j−1+bi,j
代码
int m, n, q;
const int N = 1010;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c){
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
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++)
insert(i, j, i, j, a[i][j]);
while(q--){
int x1, y1, x2, y2, c;
scanf("%d %d %d %d %d", &x1, &y1, &x2, &y2, &c);
insert(x1, y1, x2, y2, c);
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
a[i][j] = a[i][j-1] + a[i-1][j] - a[i-1][j-1] + b[i][j];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++)
printf("%d ", a[i][j]);
puts("");
}
return 0;
}