壹 ❀ 引

我在JS LeetCode 303. 区域和检索 - 数组不可变,一维数组的前缀和一文中,记录了一维数组求区间合的解题思路,正好还有一题的升级版,题目来自leetcode304. 二维区域和检索 - 矩阵不可变,实不相瞒,我在看题解理解的过程中就花了不少时间,所以这里会写的格外详细一点,题目描述如下:

给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。

上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。

示例:

给定 matrix = [
[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]
]
sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12

提示:

你可以假设矩阵不可变。

会多次调用 sumRegion 方法。

你可以假设 row1 ≤ row2 且 col1 ≤ col2 。

贰 ❀ 暴力解法

其实我在看此题的时候一开始就不理解为什么(row1, col1) = (2, 1)对应矩阵中的2,按照我们我们电影院找座位的习惯,应该是第二排的第一个才对,难道不是5?后来想到这里的矩阵其实是二维数组,数组索引起点是0,这才想明白对应的其实是第三排的第二个,因此左上角是2,右下角同理。

在前面一维数组前缀和的题解中,我们已经知道了如下公式(若不理解请先阅读一维数组前缀和题解):

sumRange(i, j) = preSum[j + 1] - preSum[i]

那么站在本题的角度,我们是不是也可以分别求每行数组的前缀和,再根据区间差得到二维数组的区间和呢?我们从一个简单的二维数组开始理解,如下有二维数组[[1,1,1],[1,1,1],[1,1,1]],我们需要求(1,1,2,2)的区域和:

看图就知道答案是4,所以对应到右边的一维数组前缀和中,因为有两行数组,套公式应该是:

// 有两行,因此是两个sumRange相加
sumRange(1, 2) + sumRange(1, 2)
// 等同于
preSum[2 + 1] - preSum[1] + preSum[2 + 1] - preSum[1]
// 等同于
3 - 1 + 3 - 1 = 4

那么到这里你应该自己尝试实现具体的代码逻辑,下面是基于一维数组思路的暴力解法:

/**
* @param {number[][]} matrix
*/
var NumMatrix = function(matrix) {
const preSums = [];
for(let i =0;i<matrix.length;i++){
// 分别求每行数组的前缀和
const preSum = new Array(matrix[i].length + 1);
// 为了让每位元素适用工时,将第0位设置为0是必要的
preSum[0] = 0;
// 套用公式,唯一不同的是数组变成了二维,本质没什么区别
for (let j = 1; j < preSum.length; j++) {
preSum[j] = preSum[j-1]+matrix[i][j-1];
};
preSums.push(preSum);
}
this.preSums = preSums;
}; /**
* @param {number} row1
* @param {number} col1
* @param {number} row2
* @param {number} col2
* @return {number}
*/
NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
// 从row1到row2行的前缀和都要加起来
// 每行数组的前缀和符合一维数组前缀和公式,范围其实就是(col1,col2)
// sumRange(i, j) = preSum[j + 1] - preSum[i]
let sum = 0;
for(let i = row1;i<=row2;i++){
sum += this.preSums[i][col2 + 1] - this.preSums[i][col1];
}
return sum;
};

叁 ❀ 二维数组前缀和

前面介绍了比较暴力的做法,现在我们来介绍题目期望我们的做法,也就是套用二维数组前缀和的公式去解答,公式之前没听过不重要,现在你听过了。

我们假设O代表坐标(0,0),D代表坐标(i,j)结合图解,公式如下:

S(O,D)=S(O,C)+S(O,B)−S(O,A)+D

公式的意思是,0-->D的所有元素和,等于0-->C(红色6格)的所有元素和加上O-->B(蓝色6格)的所有元素和,再减去O-->A(灰色4格)的所有元素和,再加一个D(网状格)。

之所以减去一个O-->A是因为O-->C与O-->B两个重复了一次OA,因此得减去。

如果用preSum(i,j)来表示坐标(0,0)到(i,j)的左右元素和,以上公式等同于:

preSum[i][j] = preSum[i][j−1] + preSum[i−1][j] − preSum[i−1][j−1] + matrix[i][j]

请结合上图来理解,说到底,preSum[i][j−1]就是S(O,C),也就是前面图解中的红色区域,其它同理。而这个matrix[i][j]其实就是题目提供的数组matrix的第i行的第j个元素而已。

在我写这篇文章之前,我当时的第一直觉就是用一个简单的例子来验证这个公式对不对,但比较笨比的是我用了一维数组前缀和累加的思路验证公式,然后苦思冥想了半个小时,心想是不是公式错了...

这里的图与一维数组前缀和的图不同在于,之前我们是一行行分别求前缀和,这里我本能的每增一行,都是从上一行最后一个元素的基础上加1作为起点,所以得到了这么个图,那么套入公式:

9 = 8 + 6 - 5 + 1

数学再差的同学也能立马感觉到不对,后面计算出来的数字是10,并不相等。原因其实很简单,我们在前面的图解中也解释过,8这个位置的数字不应该为8,它只应该包含前面图解中O==>C(红色区域)这6个元素,因此应该是6。

所以正确的二维数组前缀和的结果图应该是这样,这里我们直接画出来:

再来验证公式,非常正确:

9 = 6 + 6 - 4 + 1

那么这个二维数组前缀和的这个数字结构要怎么算出来呢?当然是套用我们前面preSum[i][j] = preSum[i][j−1] + preSum[i−1][j] − preSum[i−1][j−1] + matrix[i][j]这个公式算出来的。有同学可能就觉得不对了,以preSum(0,0)为例,它的答案很是1,但很明显我们没办法带入工时,因为不管是i-1还是j-1都超出了数组范围,怎么办?其实还是与一维数组前缀和思路相同,给每行每列都多加一维,且初始值设置为0,如下图:

我们再来看preSum(0,0),此时不就是0+0-0+1了么,你看这个公式是不是神了,我们要做的就是给二维数组多加一行一列,并将其都设置为0即可。

我们先来实现第一步,也就是得到一个上图这样的二维数组前缀和,我们可以先预设一个preSum的数组,它的行数应该是数组matrix行数基础加1,列数应该是matrix[i]也就是任意行的列数基础加1:

var NumMatrix = function(matrix) {
// 先创建preSums的行
const preSums = new Array(matrix.length+1);
// 再为每行添加列
for(let i =0; i < preSums.length; i++){
// 注意,这里不能写成 preSums[i] = new Array(matrix[i].length + 1);
// 因为很明显preSums比matrix多一行,i一定会超出matrix的范围,导致报错
// 之所以用0,是因为矩阵中不存在每行列数不同的情况,取第一行为标准就好了
preSums[i] = new Array(matrix[0].length + 1);
}
// 开始套用公式计算二维数组前缀和,同时需要初始化前缀和中第一排以及第一列为0
for(let i = 0; i < preSums.length; i++){
for(let j = 0; j<preSums[i].length; j++){
// 注意,只要是定0行或者第0列,这个格子就应该是0,因此preSums就应该是0
if (i === 0 || j === 0) {
// 只要i是0,不管当前j是多少都应该是0,同理不管第几行,只要j是0也就是第一列,也应该是0
preSums[i][j] = 0
} else {
// 否则就套用公式
preSums[i][j] = preSums[i-1][j] + preSums[i][j-1] - preSums[i-1][j-1] + matrix[i-1][j-1]
}
}
}
// 你可以取消这行注释查看数组结构
// console.log(preSums)
this.preSums = preSums;
};

上面代码的注释真的是我能解释的极限了,所以就不多说啥了,那么到这里我们就套用公式得到了二维数组前缀和的结果,那么这还不够啊,因为题目是要求我们得到矩阵范围区间的和,咋整呢?

我们假设求[[1,1,1],[1,1,1],[1,1,1]]的(0,0,1,1)区间范围和,答案很清楚其实就是4,因为一共四格,问题是怎么算呢?其实还是套用二维数组前缀和公式,为了方便理解,我们还是给出图示:

我们将最初的二维数组矩阵对应到右边的前缀和二维数组,要求的和其实还是灰色区域,如果我们还是用区域拆分的方式,你会发现灰色区域等于下图图示:

之所以最后要加一个0,也是因为前面减区域时0是交汇处,多减了一次得补回来一个,对应下来其实就是4-0-0+0。为啥横竖三个都是0,我怕大家不理解,其实横着的3个0不就是对应的就是一维数组[0,0,0]的前缀和,竖着的3个0不就是对应二维数组[[0],[0],[0]]的前缀和,所以横竖都是0。

因此满足公式:

preSums(r1,c1,r2,c2) = preSums[r2+1][c2+1] + preSums[r1][c1] - preSums[r1][c2+1] - preSums[r2+1][c1]

为了验证这一点,我们可以假设求数组[[1,1,1],[1,1,1],[1,1,1]]的(1,1,2,2)区间范围和,其实答案也是4,带入公式其实就是9+1-3-3

如果到这里还不能理解,那建议深呼吸,洗把脸冷静下。

结合上面的代码,最终题解其实就是:

/**
* @param {number[][]} matrix
*/
var NumMatrix = function(matrix) {
// 先创建preSums的行
const preSums = new Array(matrix.length+1);
// 再为每行添加列
for(let i =0; i < preSums.length; i++){
// 注意,这里不能写成 preSums[i] = new Array(matrix[i].length + 1);
// 因为很明显preSums比matrix多一行,i一定会超出matrix的范围,导致报错
// 之所以用0,是因为矩阵中不存在每行列数不同的情况,取第一行为标准就好了
preSums[i] = new Array(matrix[0].length + 1);
}
// 开始套用公式计算二维数组前缀和,同时需要初始化前缀和中第一排以及第一列为0
for(let i = 0; i < preSums.length; i++){
for(let j = 0; j<preSums[i].length; j++){
// 注意,只要是定0行或者第0列,这个格子就应该是0,因此preSums就应该是0
if (i === 0 || j === 0) {
// 只要i是0,不管当前j是多少都应该是0,同理不管第几行,只要j是0也就是第一列,也应该是0
preSums[i][j] = 0
} else {
// 否则就套用公式
preSums[i][j] = preSums[i-1][j] + preSums[i][j-1] - preSums[i-1][j-1] + matrix[i-1][j-1]
}
}
}
// 你可以取消这行注释查看数组结构
// console.log(preSums)
this.preSums = preSums;
}; /**
* @param {number} row1
* @param {number} col1
* @param {number} row2
* @param {number} col2
* @return {number}
*/
NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
return this.preSums[row2+1][col2+1] + this.preSums[row1][col1] - this.preSums[row1][col2+1] - this.preSums[row2+1][col1]
};

你说发现长篇大论下来,围绕的还是一个二维数组求前缀和的公式展开,可能你会觉得,我要是不知道这个公式鬼做的出来,但不管怎么说,你现在知道了这个公式,就像我们以前不知道1+1=2一样,现在知道了,以后也就知道了。如果时间久了再遇到此题,我们还是可以从最简单的九宫格开始,九个格子都是1,标好ABCDO四个点,按照区域划分的思维去推导此公式。

我现在其实挺不乐意去记录这种复杂的算法题解,比如这篇文章,从理解画图到写作,前前后后用了快5个小时,LeetCode中存在上千道这样的题目,如果记录我还要用多少时间呢?可是后来一想,我不去做,不去写,可能永远都不懂这个题目的思路,不管怎么样算是把这道题啃下来了,有收获就好,哪怕一点点。

那么到这里本文结束。

JS Leetcode 304. 二维区域和检索 - 矩阵不可变,彻底弄懂二维数组前缀和的更多相关文章

  1. Java实现 LeetCode 304 二维区域和检索 - 矩阵不可变

    304. 二维区域和检索 - 矩阵不可变 给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2). Range Sum Qu ...

  2. Leetcode 304.二维区域和检索-矩阵不可变

    二维区域和检索 - 矩阵不可变 给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2). 上图子矩阵左上角 (row1, c ...

  3. [Swift]LeetCode304. 二维区域和检索 - 矩阵不可变 | Range Sum Query 2D - Immutable

    Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper lef ...

  4. LeetCode 304. Range Sum Query 2D - Immutable 二维区域和检索 - 矩阵不可变(C++/Java)

    题目: Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper ...

  5. [Leetcode]303.区域和检索&&304.二维区域和检索

    题目 1.区域和检索: 简单题,前缀和方法 乍一看就觉得应该用前缀和来做,一个数组多次查询. 实现方法: 新建一个private数组prefix_sum[i],用来存储nums前i个数组的和, 需要找 ...

  6. [LeetCode] Range Sum Query 2D - Mutable 二维区域和检索 - 可变

    Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper lef ...

  7. [LeetCode] 304. Range Sum Query 2D - Immutable 二维区域和检索 - 不可变

    Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper lef ...

  8. [LeetCode] Range Sum Query 2D - Immutable 二维区域和检索 - 不可变

    Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper lef ...

  9. 领扣(LeetCode)二维区域和检索 个人题解

    给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2). 上图子矩阵左上角 (row1, col1) = (2, 1) ,右 ...

  10. 304 Range Sum Query 2D - Immutable 二维区域和检索 - 不可变

    给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2). 上图子矩阵左上角 (row1, col1) = (2, 1) ,右 ...

随机推荐

  1. 每天学五分钟 Liunx 110 | 存储篇:RAID

    RAID RAID 是廉价磁盘冗余阵列(Redundant Array of Inexpensive Disks)的意思.通过它可以将较小的磁盘组成较大的磁盘.   RAID 模式 RAID 有几种模 ...

  2. Clock Domain Crossing

    Clock Domain Crossing CDC问题主要有亚稳态问题,多比特信号同步,握手信号同步,异步Fifo等 Topics Describe the SoC Design Issues Und ...

  3. 2. 成功使用SQL Plus完成连接,但在使用Oracle SQL Developer连接时,发生报错ORA-12526: TNS:listener: all appropriate instances are in restricted mode

    经了解后得知,错误原因:ORA-12526: TNS: 监听程序: 所有适用例程都处于受限模式. 解决办法:使用系统管理员身份运行以下一段代码 ALTER SYSTEM DISABLE RESTRIC ...

  4. Linux-软件包管理-rpm-yum-apt

  5. Qt5.9 UI设计(六)——TitleBar功能实现

    前言 上一章介绍了ControlTreeWidget 与ControlTabWidget联动的功能,这一章我们将实现自定义 TitleBar 的功能 操作步骤 修改按键图标最大和最小值 右键按键图标, ...

  6. [转帖]DevOps & CI/CD 常见面试题汇总

    https://www.cnblogs.com/Dev0ps/p/15123168.html 什么是 DevOps答:用最简单的术语来说,DevOps 是产品开发过程中开发(Dev)和运营(Ops) ...

  7. [转帖]警惕Oracle数据库性能“隐形杀手”——Direct Path Read

    一. 简介 Oracle 的11g版本正式发布到今天已经10年有余,最新版本也已经到了20c,但是Direct Path Read(直接路径读)导致性能问题的案例仍时有发生,很多12c的用户还是经常遇 ...

  8. [转帖]鲲鹏性能优化十板斧——鲲鹏处理器NUMA简介与性能调优五步法

    https://www.cnblogs.com/huaweicloud/p/12166354.html 1.1 鲲鹏处理器NUMA简介 随着现代社会信息化.智能化的飞速发展,越来越多的设备接入互联网. ...

  9. Grafana监控OracleDB的完整过程

    Grafana监控OracleDB的完整过程 背景 两年前曾经写过一个进行Oracle 监控的简单blog 但是周天晚上尝试进行处理时发现很不完整了. 很多数据获取不到. 晚上又熬夜了好久进行处理. ...

  10. [转帖]原创经典:SQLSERVER SendStringParametersAsUnicode引发的疑案 推荐

    https://developer.aliyun.com/article/429563 简介: 上周五碰到开发的请求协助解决数据预定程序中对单头等几个表检索数据时检索条件尾数是9的数据特别慢.第一时间 ...