力扣刷题日记

力扣刷题日记13---剑指 Offer II 013. 二维子矩阵的和

给定一个二维矩阵 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)。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于力扣刷题C++常用操作,我可以给你一些常见的操作和技巧: 1. 使用 STL 容器和算法库:STL(Standard Template Library)是 C++ 标准库中的一个重要组成部分,包含了许多常用的容器和算法。在力扣刷题中,使用 STL 可以大大提高代码的效率和可读性。例如,vector 可以用来存储动态组,sort 可以用来排序等等。 2. 使用 auto 关键字:auto 关键字可以自动推导变量类型,可以减少代码量和提高可读性。例如,auto x = 1; 可以自动推导出 x 的类型为 int。 3. 使用 lambda 表达式:lambda 表达式是 C++11 中引入的一种匿名函,可以方便地定义一些简单的函对象。在力扣刷题中,使用 lambda 表达式可以简化代码,例如在 sort 函中自定义比较函。 4. 使用位运算:位运算是一种高效的运算方式,在力扣刷题中经常会用到。例如,左移运算符 << 可以用来计算 2 的幂次方,右移运算符 >> 可以用来除以 2 等等。 5. 使用递归:递归是一种常见的算法思想,在力扣刷题中也经常会用到。例如,二叉树的遍历、链表的反转等等。 6. 使用 STL 中的 priority_queue:priority_queue 是 STL 中的一个容器,可以用来实现堆。在力扣刷题中,使用 priority_queue 可以方便地实现一些需要维护最大值或最小值的算法。 7. 使用 STL 中的 unordered_map:unordered_map 是 STL 中的一个容器,可以用来实现哈希表。在力扣刷题中,使用 unordered_map 可以方便地实现一些需要快速查找和插入的算法。 8. 使用 STL 中的 string:string 是 STL 中的一个容器,可以用来存储字符串。在力扣刷题中,使用 string 可以方便地处理字符串相关的问。 9. 注意边界条件:在力扣刷题中,边界条件往往是解决问的关键。需要仔细分析目,考虑各种边界情况,避免出现错误。 10. 注意时间复杂度:在力扣刷题中,时间复杂度往往是评判代码优劣的重要标。需要仔细分析算法的时间复杂度,并尽可能优化代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值