理论
前缀和其实就是 动态规划 的思想,因为具体题目动态规划的思路画成表格来看,就是类似前缀和的样子:不断根据前面算出的值 做同样某个或某几个操作,累积计算。最后 利用滚动算出来的值 快速得出想要的答案。
preSum(前缀和) 方法能快速计算指定区间段 i - j 的元素之和。它的计算方法是从左向右遍历数组,当遍历到数组的 i
位置时,preSum 表示 i 位置左边的元素之和。
下面根据实际题目理解一下~
例题
leetcode303. 区域和检索 - 数组不可变
给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。
实现 NumArray 类:
NumArray(int[] nums) 使用数组 nums 初始化对象 int sumRange(int i, int j) 返回数组
nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], … , nums[j]))
思路分析
这是一道前缀和的裸题。
当需要我们求「某一段」区域的和的时候,我们要很自然的想到「前缀和」。
前缀和的作用就是为了帮助我们快速求某一段的和,是「差分」的逆运算。
前缀和数组 sum 的每一位记录的是当前位置距离起点位置,这连续一段的和区间和。
因此当我们要求特定的一段 [i,j] 的区域和的时候,可以直接利用前缀和数组快速求解:ans = sum[j] - sum[i - 1]。
- 由于涉及 -1 操作,为了减少一些边界处理,我们可以使前缀和数组下标从 1 开始记录,然后在进行答案计算的时候,根据源数组下标是否从 1 开始决定是否产生相应的偏移:
class NumArray {
int[] sum;
public NumArray(int[] nums) {
int n = nums.length;
// 前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
sum = new int[n + 1];
// 预处理除前缀和数组(模板部分)
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
}
public int sumRange(int i, int j) {
// 求某一段区域和 [i, j] 的模板是 `sum[j] - sum[i - 1]`(模板部分)
// 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
// `sum[j + 1] - sum[i - 1 + 1] = sum[j + 1] - sum[i]`
return sum[j + 1] - sum[i];
}
}
- 也可以仅按照n的大小开,只是下标为0的情况要单独拎出来写~
class NumArray {
public:
vector<int> sum;
public:
NumArray(vector<int>& nums) {
int n = nums.size();
if(n == 0) return;
sum.resize(n);
sum[0] = nums[0];
//求前缀和可快速找到i~j的和,不用每次都便利求和一遍
for(int i = 1; i < n; i++){
sum[i] = sum[i-1] + nums[i];
}
}
int sumRange(int i, int j) {
if(i == 0) return sum[j];
return sum[j] - sum[i-1];
}
};
/**
* Your NumArray object will be instantiated and called as such:
* NumArray* obj = new NumArray(nums);
* int param_1 = obj->sumRange(i,j);
*/
事实上,前缀和思想不仅能应用在一维,还能应用在多维上。
见下题:
leetcode304. 二维区域和检索 - 矩阵不可变
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为8。
思路分析
- 做这种初始化一次、检索多次的题目的秘诀:在初始化的时候做预处理。
1.暴力法
对二维矩阵,求子矩阵 (n*m) 的和。暴力法就是两重循环,累加求和。
每次查询花费 O(n*m)O(n∗m) 时间,n和m是子矩阵的行数和列数。查询的代价有点大。
1.第一步优化:一维前缀和
上面的暴力法其实也分了 n 步:第一行的求和,到第 n 行的求和
它们分别是 n 个一维数组。
上一题学习了一维前缀和,我们可以对这n个一维数组求前缀和,得到n个一维preSum数组。
为了节省查询的时间,我们求出整个矩阵每一行的一维preSum数组
根据定义 preSum[i] = nums[0] + nums[1] +…+nums[i],求出下图红字部分:
然后套用通式:nums[i]+…+nums[j]=preSum[j]-preSum[i-1] 就可以求出粉色子阵列的和,如下图,两个黄圈的前缀和,减去所在行的绿圈的前缀和,相加。
可见,如果想每次查询一个子阵列的和,我们可以提前求出每一行数组的一维前缀和。
查询一行的子数组的和,就是 O(1) 的复杂度
那么查询 n 行的子阵列时,就是每次查询花费 O(n),比O(n^2) 好
class NumMatrix {
public:
vector<vector<int>> preSum;//前缀和二维数组
public:
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size();//m行n列
if(m == 0) return;//记得先判断行,行为0,列就无意义,获取会报错
int n = matrix[0].size();
preSum.resize(m, vector<int>(n+1));
for(int row = 0; row < m; row++){
for(int col = 1; col < n+1; col++){
preSum[row][col] = preSum[row][col-1] + matrix[row][col-1];//按行计算一维前缀和
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for(int i = row1; i <=row2; i++){
sum += preSum[i][col2+1] - preSum[i][col1];
}
return sum;
}
};
2.第二步优化:二维前缀和
- 步骤一:求 preSum
我们先从如何求出二维空间的 preSum[i][j]。 我们定义 preSum[i][j] 表示 从[0,0] 位置到 [i,j] 位置的子矩形所有元素之和。
可以用下图帮助理解:S(O, D) = S(O, C) + S(O, B) - S(O, A) + D S(O,D)=S(O,C)+S(O,B)−S(O,A)+D
减去 S(O, A) 的原因是 S(O, C) 和 S(O, B) 中都有 S(O, A),即加了两次 S(O, A),所以需要减去一次 S(O, A)。
如果用 preSum 表示的话,对应了以下的递推公式:preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + matrix[i][j]
- 步骤二:根据 preSum 求子矩形面积
前面已经求出了数组中从 [0,0] 位置到 [i,j] 位置的 preSum。下面要利用preSum[i][j] 来快速求出任意子矩形的面积。
同样利用一张图来说明:S(A, D) = S(O, D) - S(O, E) - S(O, F) + S(O, G)
加上子矩形 S(O, G) 面积的原因是 S(O, E) 和 S(O, F) 中都有 S(O, G),即减了两次 S(O,G),所以需要加上一次 S(O, G)。
如果要求 [row1, col1] 到 [row2, col2] 的子矩形的面积的话,用preSum 对应了以下的递推公式:preSum[row2][col2] - preSum[row2][col1 - 1] - preSum[row1 - 1][col2] + preSum[row1 - 1][col1 - 1]
下面代码实现的时候,使用的 preSum 比原矩阵 matrix 多了一行一列,是为了让第 0 行与第 0 列的元素也能使用上面的递推公式。如果 preSum 矩阵大小和 martix 大小相等,则需要对第 0 行与第 0 列特殊判断。
class NumMatrix {
public:
vector<vector<int>> sums;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size();
if (m > 0) {
int n = matrix[0].size();
sums.resize(m + 1, vector<int>(n + 1));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
}
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
}
};
文中图片参考自:https://leetcode-cn.com/problems/range-sum-query-2d-immutable/solution/ru-he-qiu-er-wei-de-qian-zhui-he-yi-ji-y-6c21/ 仅作记录学习~