0304-二维区域和检索 - 矩阵不可变

Raphael Liu Lv10

给定一个二维矩阵 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
  • 最多调用 104sumRegion 方法

前言

这道题是「303. 区域和检索 - 数组不可变 」的进阶,第 303 题是在一维数组中做区域和检索,这道题是在二维矩阵中做区域和检索。

这道题有两种解法,分别是对每一行计算一维前缀和,以及对整个矩阵计算二维前缀和。

方法一:一维前缀和

第 303 题中,初始化时对数组计算前缀和,每次检索即可在 $O(1)$ 的时间内得到结果。可以将第 303 题的做法应用于这道题,初始化时对矩阵的每一行计算前缀和,检索时对二维区域中的每一行计算子数组和,然后对每一行的子数组和计算总和。

具体实现方面,创建 $m$ 行 $n+1$ 列的二维数组 sums,其中 $m$ 和 $n$ 分别是矩阵 matrix 的行数和列数,sums}[i]$ 为 matrix}[i]$ 的前缀和数组。将 sums 的列数设为 $n+1$ 的目的是为了方便计算每一行的子数组和,不需要对 col}_1=0$ 的情况特殊处理。

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NumMatrix {
int[][] sums;

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;
}
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var NumMatrix = function(matrix) {
const m = matrix.length;
if (m > 0) {
const n = matrix[0].length;
this.sums = new Array(m).fill(0).map(() => new Array(n + 1).fill(0));
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
this.sums[i][j + 1] = this.sums[i][j] + matrix[i][j];
}
}
}
};

NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
let sum = 0;
for (let i = row1; i <= row2; i++) {
sum += this.sums[i][col2 + 1] - this.sums[i][col1];
}
return sum;
};
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type NumMatrix struct {
sums [][]int
}

func Constructor(matrix [][]int) NumMatrix {
sums := make([][]int, len(matrix))
for i, row := range matrix {
sums[i] = make([]int, len(row)+1)
for j, v := range row {
sums[i][j+1] = sums[i][j] + v
}
}
return NumMatrix{sums}
}

func (nm *NumMatrix) SumRegion(row1, col1, row2, col2 int) (sum int) {
for i := row1; i <= row2; i++ {
sum += nm.sums[i][col2+1] - nm.sums[i][col1]
}
return
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NumMatrix:

def __init__(self, matrix: List[List[int]]):
m, n = len(matrix), (len(matrix[0]) if matrix else 0)
self.sums = [[0] * (n + 1) for _ in range(m)]
_sums = self.sums

for i in range(m):
for j in range(n):
_sums[i][j + 1] = _sums[i][j] + matrix[i][j]

def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
_sums = self.sums

total = sum(_sums[i][col2 + 1] - _sums[i][col1] for i in range(row1, row2 + 1))
return total
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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, vector<int>(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];
}
}
}
}

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;
}
};
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef struct {
int** sums;
int sumsSize;
} NumMatrix;

NumMatrix* numMatrixCreate(int** matrix, int matrixSize, int* matrixColSize) {
NumMatrix* ret = malloc(sizeof(NumMatrix));
ret->sums = malloc(sizeof(int*) * matrixSize);
ret->sumsSize = matrixSize;
for (int i = 0; i < matrixSize; i++) {
ret->sums[i] = malloc(sizeof(int) * (matrixColSize[i] + 1));
ret->sums[i][0] = 0;
for (int j = 0; j < matrixColSize[i]; j++) {
ret->sums[i][j + 1] = ret->sums[i][j] + matrix[i][j];
}
}
return ret;
}

int numMatrixSumRegion(NumMatrix* obj, int row1, int col1, int row2, int col2) {
int sum = 0;
for (int i = row1; i <= row2; i++) {
sum += obj->sums[i][col2 + 1] - obj->sums[i][col1];
}
return sum;
}

void numMatrixFree(NumMatrix* obj) {
for (int i = 0; i < obj->sumsSize; i++) {
free(obj->sums[i]);
}
free(obj->sums);
}

复杂度分析

  • 时间复杂度:初始化 $O(mn)$,每次检索 $O(m)$,其中 $m$ 和 $n$ 分别是矩阵 matrix 的行数和列数。
    初始化需要遍历矩阵 matrix 计算二维前缀和,时间复杂度是 $O(mn)$。
    每次检索需要对二维区域中的每一行计算子数组和,二维区域的行数不超过 $m$,计算每一行的子数组和的时间复杂度是 $O(1)$,因此每次检索的时间复杂度是 $O(m)$。

  • 空间复杂度:$O(mn)$,其中 $m$ 和 $n$ 分别是矩阵 matrix 的行数和列数。需要创建一个 $m$ 行 $n+1$ 列的前缀和数组 sums。

方法二:二维前缀和

方法一虽然利用了前缀和,但是每次检索的时间复杂度是 $O(m)$,仍然没有降到 $O(1)$。为了将每次检索的时间复杂度降到 $O(1)$,需要使用二维前缀和,在初始化的时候计算二维前缀和数组。

假设 $m$ 和 $n$ 分别是矩阵 matrix 的行数和列数。定义当 $0 \le i<m$ 且 $0 \le j<n$ 时,$f(i,j)$ 为矩阵 matrix 的以 $(i,j)$ 为右下角的子矩阵的元素之和:

$$
f(i,j)=\sum\limits_{p=0}^i \sum\limits_{q=0}^j \textit{matrix}[p][q]
$$

当 $i=0$ 或 $j=0$ 时,计算 $f(i,j)$ 只需要对矩阵 matrix 的最上边的行和最左边的列分别计算前缀和即可。当 $i$ 和 $j$ 都大于 $0$ 时,如何计算 $f(i,j)$ 的值?

当 $i$ 和 $j$ 都大于 $0$ 时,假设计算 $f(i,j)$ 时已经知道了 $f(i-1,j)$、$f(i,j-1)$ 和 $f(i-1,j-1)$ 的值。为了计算 $f(i,j)$,自然而然会想到使用 $f(i-1,j)$、$f(i,j-1)$ 和 matrix}[i][j]$ 的值。

注意到 $f(i-1,j)$ 和 $f(i,j-1)$ 这两项涉及到的矩阵 matrix 的元素有重合,矩阵 matrix 的以 $(i-1,j-1)$ 为右下角的子矩阵都在这两项中出现。因此如果计算 $f(i-1,j)+f(i,j-1)+\textit{matrix}[i][j]$,则该结果值比 $f(i,j)$ 多了 $f(i-1,j-1)$,因此 $f(i,j)$ 的计算如下:

$$
f(i,j)=f(i-1,j)+f(i,j-1)-f(i-1,j-1)+\textit{matrix}[i][j]
$$

具体推导如下:

$$
\begin{aligned}
&\quad \ f(i,j) \
&=\sum\limits_{p=0}^{i-1} \sum\limits_{q=0}^{j-1} \textit{matrix}[p][q]+\sum\limits_{p=0}^{i-1} \textit{matrix}[p][j]+\sum\limits_{q=0}^{j-1} \textit{matrix}[i][q]+\textit{matrix}[i][j] \
&=\Big(\sum\limits_{p=0}^{i-1} \sum\limits_{q=0}^{j-1} \textit{matrix}[p][q]+\sum\limits_{p=0}^{i-1} \textit{matrix}[p][j]\Big) \
&\quad+\Big(\sum\limits_{p=0}^{i-1} \sum\limits_{q=0}^{j-1} \textit{matrix}[p][q]+\sum\limits_{q=0}^{j-1} \textit{matrix}[i][q]\Big) \
&\quad-\sum\limits_{p=0}^{i-1} \sum\limits_{q=0}^{j-1} \textit{matrix}[p][q] \
&\quad+\textit{matrix}[i][j] \
&=\sum\limits_{p=0}^{i-1} \sum\limits_{q=0}^j \textit{matrix}[p][q]+\sum\limits_{p=0}^i \sum\limits_{q=0}^{j-1} \textit{matrix}[p][q]-\sum\limits_{p=0}^{i-1} \sum\limits_{q=0}^{j-1} \textit{matrix}[p][q]+\textit{matrix}[i][j] \
&=f(i-1,j)+f(i,j-1)-f(i-1,j-1)+\textit{matrix}[i][j]
\end{aligned}
$$

因此在初始化的时候,即可对所有 $0 \le i<m$ 和 $0 \le j<n$ 计算得到 $f(i,j)$ 的值。

fig1

检索时,应利用预处理得到的 $f$ 的值。当 row}_1=0$ 且 col}_1=0$ 时,sumRegion}(\textit{row}_1,\textit{col}_1,\textit{row}_2,\textit{col}_2)=f(\textit{row}_2,\textit{col}_2)$。

当 row}_1 \le \textit{row}_2$ 且 col}_1 \le \textit{col}_2$ 时,sumRegion}(\textit{row}_1,\textit{col}_1,\textit{row}_2,\textit{col}_2)$ 可以写成如下形式:

$$
\begin{aligned}
&\quad \ \text{sumRegion}(\textit{row}_1,\textit{col}_1,\textit{row}_2,\textit{col}_2) \
&=\text{sumRegion}(0,0,\textit{row}_2,\textit{col}_2) \
&\quad-\text{sumRegion}(0,\textit{col}_1,\textit{row}_1-1,\textit{col}_2) \
&\quad-\text{sumRegion}(\textit{row}_1,0,\textit{row}_2,\textit{col}_1-1) \
&\quad-\text{sumRegion}(0,0,\textit{row}_1-1,\textit{col}_1-1) \
&=\text{sumRegion}(0,0,\textit{row}_2,\textit{col}_2) \
&\quad-(\text{sumRegion}(0,\textit{col}_1,\textit{row}_1-1,\textit{col}_2)+\text{sumRegion}(0,0,\textit{row}_1-1,\textit{col}_1-1)) \
&\quad-(\text{sumRegion}(\textit{row}_1,0,\textit{row}_2,\textit{col}_1-1)+\text{sumRegion}(0,0,\textit{row}_1-1,\textit{col}_1-1)) \
&\quad-\text{sumRegion}(0,0,\textit{row}_1-1,\textit{col}_1-1) \
&\quad+2 \times \text{sumRegion}(\textit{row}_1,0,\textit{row}_2,\textit{col}_1-1) \
&=\text{sumRegion}(0,0,\textit{row}_2,\textit{col}_2) \
&\quad-\text{sumRegion}(0,0,\textit{row}_1-1,\textit{col}_2) \
&\quad-\text{sumRegion}(0,0,\textit{row}_2,\textit{col}_1-1) \
&\quad+\text{sumRegion}(0,0,\textit{row}_1-1,\textit{col}_1-1) \
&=f(\textit{row}_2,\textit{col}_2)-f(\textit{row}_1-1,\textit{col}_2)-f(\textit{row}_2,\textit{col}_1-1)+f(\textit{row}_1-1,\textit{col}_1-1)
\end{aligned}
$$

<ppt1,ppt2,ppt3,ppt4>

即可在 $O(1)$ 时间内得到 sumRegion}(\textit{row}_1,\textit{col}_1,\textit{row}_2,\textit{col}_2)$ 的值。

具体实现方面,创建 $m+1$ 行 $n+1$ 列的二维数组 sums,其中 sums}[i+1][j+1]$ 的值为上述 $f(i,j)$ 的值。

将 sums 的行数和列数分别设为 $m+1$ 和 $n+1$ 的目的是为了方便计算 sumRegion}(\textit{row}_1,\textit{col}_1,\textit{row}_2,\textit{col}_2)$ ,不需要对 row}_1=0$ 和 col}_1=0$ 的情况特殊处理。此时有:

$$
\begin{aligned}
&\quad \ \text{sumRegion}(\textit{row}_1,\textit{col}_1,\textit{row}_2,\textit{col}_2) \
&=\textit{sums}[\textit{row}_2+1][\textit{col}_2+1]-\textit{sums}[\textit{row}_1][\textit{col}_2+1]-\textit{sums}[\textit{row}_2+1][\textit{col}_1]+\textit{sums}[\textit{row}_1][\textit{col}_1]
\end{aligned}
$$

[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NumMatrix {
int[][] sums;

public NumMatrix(int[][] matrix) {
int m = matrix.length;
if (m > 0) {
int n = matrix[0].length;
sums = new int[m + 1][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];
}
}
}
}

public 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];
}
}
[sol2-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var NumMatrix = function(matrix) {
const m = matrix.length;
if (m > 0) {
const n = matrix[0].length;
this.sums = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
this.sums[i + 1][j + 1] = this.sums[i][j + 1] + this.sums[i + 1][j] - this.sums[i][j] + matrix[i][j];
}
}
}
};

NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
return this.sums[row2 + 1][col2 + 1] - this.sums[row1][col2 + 1] - this.sums[row2 + 1][col1] + this.sums[row1][col1];
};
[sol2-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type NumMatrix struct {
sums [][]int
}

func Constructor(matrix [][]int) NumMatrix {
m := len(matrix)
if m == 0 {
return NumMatrix{}
}
n := len(matrix[0])
sums := make([][]int, m+1)
sums[0] = make([]int, n+1)
for i, row := range matrix {
sums[i+1] = make([]int, n+1)
for j, v := range row {
sums[i+1][j+1] = sums[i+1][j] + sums[i][j+1] - sums[i][j] + v
}
}
return NumMatrix{sums}
}

func (nm *NumMatrix) SumRegion(row1, col1, row2, col2 int) int {
return nm.sums[row2+1][col2+1] - nm.sums[row1][col2+1] - nm.sums[row2+1][col1] + nm.sums[row1][col1]
}
[sol2-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NumMatrix:

def __init__(self, matrix: List[List[int]]):
m, n = len(matrix), (len(matrix[0]) if matrix else 0)
self.sums = [[0] * (n + 1) for _ in range(m + 1)]
_sums = self.sums

for i in range(m):
for j in range(n):
_sums[i + 1][j + 1] = _sums[i][j + 1] + _sums[i + 1][j] - _sums[i][j] + matrix[i][j]

def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
_sums = self.sums

return _sums[row2 + 1][col2 + 1] - _sums[row1][col2 + 1] - _sums[row2 + 1][col1] + _sums[row1][col1]
[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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];
}
};
[sol2-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct {
int** sums;
int sumsSize;
} NumMatrix;

NumMatrix* numMatrixCreate(int** matrix, int matrixSize, int* matrixColSize) {
NumMatrix* ret = malloc(sizeof(NumMatrix));
ret->sums = malloc(sizeof(int*) * (matrixSize + 1));
ret->sumsSize = matrixSize + 1;
int n = matrixSize ? matrixColSize[0] : 0;
for (int i = 0; i <= matrixSize; i++) {
ret->sums[i] = malloc(sizeof(int) * (n + 1));
memset(ret->sums[i], 0, sizeof(int) * (n + 1));
}
for (int i = 0; i < matrixSize; i++) {
for (int j = 0; j < matrixColSize[i]; j++) {
ret->sums[i + 1][j + 1] = ret->sums[i][j + 1] + ret->sums[i + 1][j] - ret->sums[i][j] + matrix[i][j];
}
}
return ret;
}

int numMatrixSumRegion(NumMatrix* obj, int row1, int col1, int row2, int col2) {
return obj->sums[row2 + 1][col2 + 1] - obj->sums[row1][col2 + 1] - obj->sums[row2 + 1][col1] + obj->sums[row1][col1];
}

void numMatrixFree(NumMatrix* obj) {
for (int i = 0; i < obj->sumsSize; i++) {
free(obj->sums[i]);
}
free(obj->sums);
}

复杂度分析

  • 时间复杂度:初始化 $O(mn)$,每次检索 $O(1)$,其中 $m$ 和 $n$ 分别是矩阵 matrix 的行数和列数。
    初始化需要遍历矩阵 matrix 计算二维前缀和,时间复杂度是 $O(mn)$。
    每次检索的时间复杂度是 $O(1)$。

  • 空间复杂度:$O(mn)$,其中 $m$ 和 $n$ 分别是矩阵 matrix 的行数和列数。需要创建一个 $m+1$ 行 $n+1$ 列的二维前缀和数组 sums。

 Comments
On this page
0304-二维区域和检索 - 矩阵不可变