给定一个二维矩阵 matrix,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。
实现 NumMatrix 类:
- NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
- int sumRegion(int row1, int col1, int row2, int col2) 返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩阵的元素总和。
示例1:
输入:
[“NumMatrix”,“sumRegion”,“sumRegion”,“sumRegion”]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出:
[null, 8, 11, 12]
解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 200
-105 <= matrix[i][j] <= 105
0 <= row1 <= row2 < m
0 <= col1 <= col2 < n
最多调用 104 次 sumRegion 方法
思路1:暴力解法
首先,用给定矩阵初始化一个相同的矩阵。接下来,在 左上角 (row1, col1) 和 右下角 (row2, col2) 的子矩阵进行双层 for 循环遍历,计算锁要求的子矩阵内的元素和。
代码如下:
class NumMatrix {
public:
vector<vector<int>> v;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
v.resize(m, vector<int>(n));
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j)
v[i][j] = matrix[i][j];
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
int ret = 0;
for(int i = row1; i <= row2; ++i){
for(int j = col1; j <= col2; ++j)
ret += v[i][j];
}
return ret;
}
};
但是这样带来的问题是,每次调用 “sumRegion” 都需要进行两次 for 循环遍历,而一次的时间复杂度为:O((row2 - row1) * (col2 - col1))。这就会造成时间复杂度大大增加。
思路2:一维前缀和
前面几道题我们已经写过好几次 前缀和,使用该方法可以大大减少时间复杂度。
这里首先介绍 一维前缀和,与一维数组的前缀和类似。矩阵的一维前缀和就是,每行的前缀和之与当前行有关,与其他行无关。举个例子:
现有矩阵 v = [ [ 1, 2, 3, 4 ], [ 5, 6, 7, 8], [ 9, 10, 11, 12], [ 13, 14, 15, 16] ];
那么该矩阵的一维前缀和矩阵为: vsum = [ [ 1, 3, 6, 10 ], [ 5, 11, 18, 26], [ 9, 19, 30, 42], [ 13, 27, 42, 58] ];
获得了该 前缀和矩阵 vsum 后,可以只进行一次 for 循环就找出子矩阵的和。继续上边的例子,例如我们要求 [ 1, 1] 与 [ 3, 3 ] 这个子矩阵的元素和,我们可以分别计算 第 2、3、4 行的元素和, 即 vsum[1][3] - vsum[1][1]、 vsum[2][3] - vsum[2][1]、vsum[3][3] - vsum[3][1],这三个值相加即为最终答案。可以看出,利用 一维前缀和 可以把双层 for 循环 变为单层。
需要注意的是,在计算前缀和矩阵的时候,可以在最前边加上全为 0 的列,这样能够方便计算从第 0 列开始的子矩阵的元素和。
代码如下:
class NumMatrix {
public:
vector<vector<int>> vsum;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vsum.resize(m, vector<int>(n + 1, 0));
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j)
vsum[i][j + 1] = vsum[i][j] + matrix[i][j];
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
int ret = 0;
for(int i = row1; i <= row2; ++i){
ret += vsum[i][col2 + 1] - vsum[i][col1];
}
return ret;
}
};
时间复杂度:O(mn);空间复杂度:O(mn)。
思路3:二维前缀和
我们可以对思路 2 的方法进一步优化,把获得子矩阵元素和的时间复杂度变为 O(1)。这需要利用 二维前缀和 实现。
用 思路2 中的例子,现有矩阵 v = [ [ 1, 2, 3, 4 ], [ 5, 6, 7, 8], [ 9, 10, 11, 12], [ 13, 14, 15, 16] ];
二维前缀和就是从 [ 0, 0 ] 到 当前元素 所组成的子矩阵的元素和(不只考虑当前行,还要考虑上边的所有行)。上述矩阵的二维前缀和矩阵为: vsum = [ [ 1, 3, 6, 10 ], [ 6, 14, 24, 36], [ 15, 33, 54, 78], [ 28, 60, 96, 136] ];
这里有个公式计算二维前缀和,即 vsum[i + 1][j + 1] = vsum[i][j + 1] + vsum[i + 1][j] - vsum[i][j] + matrix[i][j];
当前元素的前缀和 = 上一行同一列元素的前缀和 + 当前行前一列元素的前缀和 - 前一行前一列的前缀和(该数值加了两次,要减去重复的一次) + 当前元素;
用图来说更直观:
假设我们现在要求 [ 1, 1 ] 位置的二维前缀和,根据公式,result = vsum[0][1](蓝色框中元素和) + vsum[1][0](红色框中元素和) - vsum[0][0] + matrix[1][1] = 3 + 6 - 1 + 6 = 14。因为前缀和 vsum[0][1] 和 vsum[1][0] 在计算时都考虑了 [ 0, 0 ] 位置的前缀和,因此我们要减掉该前缀和,所得即为正确答案,即 [ 0, 0 ] 到 [ 1, 1 ] 这个子矩阵的元素和。
接下来要获得给定范围的子矩阵的元素和,用公式 vsum[row2 + 1][col2 + 1] - vsum[row1][col2 + 1] - vsum[row2 + 1][col1] + vsum[row1][col1]; 来计算。
还是用图来解释:
假设我们要获得 [ 1, 1 ] 到 [ 2, 2 ] 这个子矩阵的元素和,根据公式 ,result = vsum[2][2](整个矩阵的元素和) - vsum[0][2](蓝色框中元素和) - vsum[2][0] (红色框中元素和)+ vsum[row1][col1] = 54 - 6 - 15 + 1 = 34。由于前缀和 vsum[0][2] 和 vsum[2][0] 在计算时都考虑了 [ 0, 0 ] 位置的前缀和,减的时候减了两次,因此我们要加上这个重复的前缀和,所得即为正确答案,即 [ 1, 1 ] 到 [ 2, 2 ] 这个子矩阵的元素和。
与思路2类似,这里也需要在最上边和最左边分别加上一行 和 一列为 0 的列,方便进行计算。(上图中没有加,理解意思就成)
代码如下:
class NumMatrix {
public:
vector<vector<int>> vsum;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vsum.resize(m + 1, vector<int>(n + 1, 0));
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j)
vsum[i + 1][j + 1] = vsum[i][j + 1] + vsum[i + 1][j] - vsum[i][j] + matrix[i][j];
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return vsum[row2 + 1][col2 + 1] - vsum[row1][col2 + 1] - vsum[row2 + 1][col1] + vsum[row1][col1];
}
};
时间复杂度:O(mn),这个是初始化的复杂度,后续计算子矩阵元素和的复杂度为O(1);空间复杂度:O(mn)。