1. 区域和检索 - 数组不可变
(1)前缀和
由于涉及 -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];
}
//sum数组中存的是前缀和,比如nums = [1,2,3,4] 输出:[1,3,6,10],但是注意sum的下标是从1开始
public int sumRange(int i, int j) {
// 求某一段区域和 [i, j] 的模板是 sum[j] - sum[i - 1](模板部分)
// 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
i++;
j++;
return sum[j] - sum[i - 1]; //减1是为了包含i,j两点(主要是i)
}
}
下标也可以从0开始:
class NumArray {
int[] sum;
int[] nums;
public NumArray(int[] nums) {
int n = nums.length;
//前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
this.nums = nums;
sum = new int[n];
sum[0] = nums[0];
for (int i = 1; i < n; i++)
sum[i] = sum[i - 1] + nums[i];
}
//sum数组中存的是前缀和,比如nums = [1,2,3,4] 输出:[1,3,6,10]
//下标从1开始
public int sumRange(int i, int j) {
return sum[j] - sum[i] + nums[i];
}
}
(2)一维前缀和模板
下标从1开始:
// 预处理前缀和数组
{
sum = new int[n + 1];
for (int i = 1; i <= n; i++)
sum[i] = sum[i - 1] + nums[i - 1];
}
// 计算 [i, j] 结果
{
i++;
j++;
ans = sum[j] - sum[i - 1];
}
下标从0开始:
// 预处理前缀和数组
int[] nums;
{
this.nums = nums;
sum = new int[n];
for (int i = 1; i < n; i++)
sum[i] = sum[i - 1] + nums[i];
}
// 计算 [i, j] 结果
{
ans = sum[j] - sum[i] + nums[i];
}
2. 二维区域和检索 - 矩阵不可变
(1)一维 前缀和
对二维矩阵,求子矩阵 (n*m) 的和。暴力法就是两重循环,累加求和。
每次查询花费 O(n*m) 时间,n和m是子矩阵的行数和列数。查询的代价有点大。
class NumMatrix {
int[][] sums;
//创建 m 行n+1 列的二维数组 sums,其中 m 和 n 分别是矩阵matrix 的行数和列数
//sums[i] 为 matrix[i] 的前缀和数组。将sums 的列数设为 n+1 的目的是为了方便计算每一行的子数组和,不需要col=0 的情况特殊处理
public NumMatrix(int[][] matrix) {
int m = matrix.length;
if (m > 0) {
int n = matrix[0].length;
sums = new int[m][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i][j + 1] = sums[i][j] + matrix[i][j];
}
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for (int i = row1; i <= row2; i++) {
sum += sums[i][col2 + 1] - sums[i][col1];
}
return sum;
}
}
(2)优化
- 上面的暴力法其实也分了 n 步:第一行的求和,到第 n 行的求和
- 它们分别是 n 个一维数组。
- 我们学习了一维前缀和,我们可以对这n个一维数组求前缀和,得到n个一维preSum数组。
- 为了节省查询的时间,我们求出整个矩阵每一行的一维preSum数组
- 根据定义 preSum[i] = nums[0] + nums[1] +…+nums[i]preSum[i]=nums[0]+nums[1]+…+nums[i] ,求出下图红字部分:
然后套用通式:nums[i]+…+nums[j] = preSum[j]−preSum[i−1]
即可求出粉色子阵列的和,计算情况如下图。
可见,如果想多次查询子阵列的和,我们可以提前求出每一行数组的一维前缀和。
那么查询阶段,求出一行子数组的求和,就只是 O(1)O(1)
查询 n 行的子阵列,每次就查询花费 O(n) 比 O(n2) 好
class NumMatrix {
int[][] A;
int row;
int column;
int[][] presum;
boolean flag=true;
public NumMatrix(int[][] matrix) {
A = matrix;
row = matrix.length;
if (row != 0)
column = matrix[0].length;
else
flag = false;
presum = new int[row][column+1];
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
presum[i][j+1]=presum[i][j]+A[i][j];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
if (flag == false)
return 0;
int sum = 0;
for (int i = row1; i <= row2; i++) {
sum += presum[i][col2+1] - presum[i][col1];
}
return sum;
}
}
(3)继续优化
未完待续。