前缀和和差分
1. 前缀和和差分原理
原理
前缀和
-
前缀和数组要求下标从1 开始,这样方便计算。 -
一维前缀和
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
差分:为了将区间操作变为单点操作
A是原数组,B是差分数组,给区间A[l, r]中的每个数加上c,等价于:B[l] += c, B[r + 1] -= c
A是原数组,B是差分数组,给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c,等价于:
B[x1, y1] += c, B[x2 + 1, y1] -= c, B[x1, y2 + 1] -= c, B[x2 + 1, y2 + 1] += c
2. AcWing上的前缀和和差分题目
AcWing 795. 前缀和
问题描述
分析
代码
#include <cstdio>
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;
}
AcWing 796. 子矩阵的和
问题描述
分析
-
二维前缀和模板题。 -
数据初始读入到s 中,然后在s 的基础上计算二维前缀和,即s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] 。 -
以(x1, y1) 为左上角,(x2, y2) 为右下角的子矩阵的和为:s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1] 。
代码
#include <cstdio>
const int N = 1010;
int n, m, q;
int 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", &s[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];
while (q--) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
return 0;
}
AcWing 797. 差分
问题描述
分析
b
[
1
]
=
a
[
1
]
b
[
2
]
=
a
[
2
]
?
a
[
1
]
.
.
.
b
[
n
]
=
a
[
n
]
?
a
[
n
?
1
]
b[1] = a[1] \\ b[2] = a[2] - a[1] \\ ... \\ b[n] = a[n] - a[n-1]
b[1]=a[1]b[2]=a[2]?a[1]...b[n]=a[n]?a[n?1]
代码
#include <cstdio>
const int N = 100010;
int n, m;
int a[N];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = n; i; i--) a[i] -= a[i - 1];
while (m--) {
int l, r, c;
scanf("%d%d%d", &l, &r, &c);
a[l] += c, a[r + 1] -= c;
}
for (int i = 1; i <= n; i++) a[i] += a[i - 1];
for (int i = 1; i <= n; i++) printf("%d ", a[i]);
return 0;
}
AcWing 798. 差分矩阵
问题描述
分析
-
给定原矩阵a[i, j] ,构造差分矩阵b[i, j] ,使得a[][] 是b[][] 的二维前缀和。 -
a 是原数组,b 是差分数组,给以(x1, y1) 为左上角,(x2, y2) 为右下角的子矩阵中的所有元素加上c ,等价于:b[x1, y1] += c, b[x2 + 1, y1] -= c, b[x1, y2 + 1] -= c, b[x2 + 1, y2 + 1] += c 。 -
如何根据a 数组推出b 数组呢?初始b 为全0 ,然后直接遍历一遍a ,在b 插入对应元素即可。
代码
#include <cstdio>
const int N = 1010;
int n, m, q;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= 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]);
insert(i, j, i, j, a[i][j]);
}
for (int i = 0; i < q; i++) {
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 - 1][j] + a[i][j - 1] - 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;
}
AcWing 99. 激光炸弹
问题描述
分析
-
本题相当于让求解一个(R-1)*(R-1) 的子矩阵的和的最大值,因为目标的坐标在[0~5000] 之间,因此可以使用一个二维数组存储每个位置的价值。 -
因为前缀和需要空出(0, 0) ,因此读入数据的时候横纵坐标加上一个1 的偏移量。
代码
#include <iostream>
using namespace std;
const int N = 5010;
int n, R;
int s[N][N];
int main() {
scanf("%d%d", &n, &R);
R = min(5001, R);
for (int i = 0; i < n; i++) {
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
x++, y++;
s[x][y] += w;
}
for (int i = 1; i <= 5001; i++)
for (int j = 1; j <= 5001; j++)
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
int res = 0;
for (int i = R; i <= 5001; i++)
for (int j = R; j <= 5001; j++)
res = max(res, s[i][j] - s[i - R][j] - s[i][j - R] + s[i - R][j - R]);
printf("%d\n", res);
return 0;
}
AcWing 100. 增减序列
问题描述
分析
-
因为本题最终变为的数一定在int 范围内,因此不会出现高精度的问题。 -
本题需要考虑原数组a 的差分数组b ,原数组a 全部为0 ,等价于:差分数组b 中b[1] 任意,b[2]~b[n] 全部为0 。 -
因此转化之后问题变为了:(1)至少操作多少次,可以使得b[2]~b[n] 全部为0 ;(2)有多少中结果,等价于b[1] 有多少种值。 -
对原数组a[1]~a[n] 的操作会影响b[1]~b[n+1] 一共n+1 个数。我们将对数组a 的操作根据范围分为四大类: (1)
2
≤
l
≤
r
≤
n
?
1
2 \le l \le r \le n-1
2≤l≤r≤n?1:让b[2]~b[n] 中某个数加一,另一个数减一; (2)
l
=
1
,
r
≤
n
?
1
l=1, r \le n-1
l=1,r≤n?1:b[1] 加一(减一),b[2]~b[n] 中的某个数减一(加一); (3)
2
≤
l
,
r
=
n
2 \le l, r = n
2≤l,r=n:b[2]~b[n] 中的某个数减一(加一),b[n+1] 加一(减一); (4)
l
=
1
,
r
=
n
l=1, r=n
l=1,r=n:相当于让数组a 中所有元素加一或者减一,无意义。 -
为了使得b[2]~b[n] 全部变为0 ,我们尽量使用第(1)中操作,即如果b[2]~b[n] 中既有正数又有负数,可以使用操作(1),否则如果只有正数或者负数使用操作(2)或者(3)即可。 -
对于差分数组b[2~n] ,首先求出所有正数的和p ,再求出所有负数绝对值的和q ,可以使用第(1)中操作min(p, q) 次,还剩余|p-q| 的正数或者负数,可以使用操作(2)或(3),因此最少的操作次数为min(p, q) + |p-q|=max(p, q) 。 -
因为最后剩余|p-q| 次操作,这些操作可以是(2)或者(3),操作(2)会改变b[1] 的值,因此b[1] 可以取|p-q|+1 种取值。
代码
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int a[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = n; i; i--) a[i] -= a[i - 1];
LL p = 0, q = 0;
for (int i = 2; i <= n; i++)
if (a[i] > 0) p += a[i];
else q -= a[i];
printf("%lld\n", max(p, q));
printf("%lld\n", abs(p - q) + 1);
return 0;
}
3. 力扣上的前缀和和差分题目
Leetcode 0303 区域和检索 - 数组不可变
题目描述:Leetcode 0303 区域和检索 - 数组不可变

分析
-
本题的考点:前缀和。 -
前缀和下标都是从1 开始的。 -
因为本题中数组中的数据不变,因此可以使用一个数组s 记录原数组nums 的前缀和,即s[i] 表示nums 前i 个数据之和。 -
有了数组s ,则如果要求解nums[i...j] 之间的元素和,则首项让i++, j++ 最后返回s[j]-s[i-1] 即可。
代码
class NumArray {
public:
vector<int> s;
NumArray(vector<int> &nums) {
int n = nums.size();
s.resize(n + 1);
for (int i = 0; i < n; i++) s[i + 1] = s[i] + nums[i];
}
int sumRange(int i, int j) {
i++, j++;
return s[j] - s[i - 1];
}
};
class NumArray {
private int[] s;
public NumArray(int[] nums) {
s = new int[nums.length + 1];
s[0] = 0;
for (int i = 1; i < s.length; i++) s[i] = s[i - 1] + nums[i - 1];
}
public int sumRange(int i, int j) {
i++; j++;
return s[j] - s[i - 1];
}
}
时空复杂度分析
Leetcode 0304 二维区域和检索 - 矩阵不可变
题目描述:Leetcode 0304 二维区域和检索 - 矩阵不可变

分析
-
本题的考点:二维前缀和。 -
前缀和下标都是从1 开始的。 -
这里使用二维数组s[i][j] 表示从左上角(0, 0) 到(i-1, j - 1) 这个
i
×
j
i \times j
i×j 矩形中的数据之和,数组s 可以递推求解,递推公式如下:s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1] 。 -
求出s 后,如果让我们求出(x1, y1) 到(x2, y2) 之间矩形中的元素和,则首先让x1++, y1++, x2++, y2++ ,然后返回s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] 即可。
代码
class NumMatrix {
public:
vector<vector<int>> s;
NumMatrix(vector<vector<int>> &matrix) {
if (matrix.empty() || matrix[0].empty()) return;
int n = matrix.size(), m = matrix[0].size();
s = vector<vector<int>>(n + 1, vector<int>(m + 1));
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] + matrix[i - 1][j - 1];
}
int sumRegion(int x1, int y1, int x2, int y2) {
x1++, y1++, x2++, y2++;
return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1];
}
};
class NumMatrix {
int[][] s;
public NumMatrix(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) return;
int n = matrix.length, m = matrix[0].length;
s = new int[n + 1][m + 1];
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] + matrix[i - 1][j - 1];
}
public int sumRegion(int x1, int y1, int x2, int y2) {
x1++; y1++; x2++; y2++;
return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1];
}
}
时空复杂度分析
|