文章目录
题目
标题和出处
标题:01 矩阵
出处:542. 01 矩阵
难度
4 级
题目描述
要求
给定一个 m × n \texttt{m} \times \texttt{n} m×n 的二进制矩阵 mat \texttt{mat} mat,返回每个单元格到最近的 0 \texttt{0} 0 的距离。
两个相邻单元格之间的距离为 1 \texttt{1} 1。
示例
示例 1:

输入: mat = [[0,0,0],[0,1,0],[0,0,0]] \texttt{mat = [[0,0,0],[0,1,0],[0,0,0]]} mat = [[0,0,0],[0,1,0],[0,0,0]]
输出: [[0,0,0],[0,1,0],[0,0,0]] \texttt{[[0,0,0],[0,1,0],[0,0,0]]} [[0,0,0],[0,1,0],[0,0,0]]
示例 2:

输入: mat = [[0,0,0],[0,1,0],[1,1,1]] \texttt{mat = [[0,0,0],[0,1,0],[1,1,1]]} mat = [[0,0,0],[0,1,0],[1,1,1]]
输出: [[0,0,0],[0,1,0],[1,2,1]] \texttt{[[0,0,0],[0,1,0],[1,2,1]]} [[0,0,0],[0,1,0],[1,2,1]]
数据范围
- m = mat.length \texttt{m} = \texttt{mat.length} m=mat.length
- n = mat[i].length \texttt{n} = \texttt{mat[i].length} n=mat[i].length
- 1 ≤ m, n ≤ 10 4 \texttt{1} \le \texttt{m, n} \le \texttt{10}^\texttt{4} 1≤m, n≤104
- 1 ≤ m × n ≤ 10 4 \texttt{1} \le \texttt{m} \times \texttt{n} \le \texttt{10}^\texttt{4} 1≤m×n≤104
- mat[i][j] \texttt{mat[i][j]} mat[i][j] 为 0 \texttt{0} 0 或 1 \texttt{1} 1
- mat \texttt{mat} mat 中至少有一个 0 \texttt{0} 0
解法一
思路和算法
对于矩阵中的 0 0 0,最近的 0 0 0 即为自身,因此 0 0 0 到最近的 0 0 0 的距离是 0 0 0,只需要考虑每个 1 1 1 到最近的 0 0 0 的距离。
如果矩阵中只有一个 0 0 0,则每个 1 1 1 的最近的 0 0 0 就是唯一的 0 0 0,到最近的 0 0 0 距离即为到唯一的 0 0 0 的距离。
如果矩阵中有多个 0 0 0,则对于每个 1 1 1,需要计算到每个 0 0 0 的距离,其中的最小距离为当前 1 1 1 到最近的 0 0 0 的距离。如果直接计算距离,则时间复杂度过高,需要优化。
由于广度优先搜索的遍历顺序是和起始点的距离递增的顺序,因此可以使用广度优先搜索计算每个 1 1 1 到最近的 0 0 0 的距离,做法是从所有的 0 0 0 出发执行多源广度优先搜索,搜索过程中记录矩阵中的每个位置到最近的 0 0 0 的距离。
矩阵中的每个 0 0 0 到最近的 0 0 0 的距离都是 0 0 0。对于矩阵中的每个 1 1 1,如果在第 k k k 轮被首次访问到,则当前 1 1 1 到最近的 0 0 0 的距离是 k k k。理由如下:如果当前 1 1 1 到最近的 0 0 0 的距离小于 k k k,则从最近的 0 0 0 开始访问到当前 1 1 1 的轮数一定小于 k k k,因此当前 1 1 1 被首次访问到的轮数小于 k k k,与当前 1 1 1 在第 k k k 轮被首次访问到矛盾。因此矩阵中的每个 1 1 1 被首次访问到的轮数即为到最近的 0 0 0 的距离。
遍历结束之后,即可得到每个单元格到最近的 0 0 0 的距离。
实现方面有以下两点说明。
-
对于每个单元格需要向四个方向遍历,可以创建方向数组实现四个方向的遍历。
-
广度优先搜索需要记录每个单元格是否被访问过,这道题由于需要计算每个单元格到最近的 0 0 0 的距离,因此可以根据结果数组中的元素判断每个单元格是否被访问过,不需要额外创建与矩阵相同的二维数组记录每个单元格是否被访问过。具体做法是,将矩阵中的每个 0 0 0 对应的结果数组中的元素初始化为 0 0 0,将矩阵中的每个 1 1 1 对应的结果数组中的元素初始化为无穷大,则结果数组中的值为无穷大的元素表示该单元格未访问。
代码
java
class Solution {
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
static final int INFINITY = Integer.MAX_VALUE / 2;
public int[][] updateMatrix(int[][] mat) {
int m = mat.length, n = mat[0].length;
int[][] distances = new int[m][n];
Queue<int[]> queue = new ArrayDeque<int[]>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 0) {
queue.offer(new int[]{i, j});
} else {
distances[i][j] = INFINITY;
}
}
}
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int row = cell[0], col = cell[1];
for (int[] dir : dirs) {
int newRow = row + dir[0], newCol = col + dir[1];
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && distances[newRow][newCol] == INFINITY) {
distances[newRow][newCol] = distances[row][col] + 1;
queue.offer(new int[]{newRow, newCol});
}
}
}
return distances;
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 mat \textit{mat} mat 的行数和列数。广度优先搜索最多需要访问每个单元格一次。
-
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 mat \textit{mat} mat 的行数和列数。队列需要 O ( m n ) O(mn) O(mn) 的空间。
解法二
预备知识
该解法涉及到动态规划。
动态规划是求解决策过程最优化的过程,其核心思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。动态规划适用于存在重叠子问题的情况,当一个子问题被解决时,将该子问题的答案记录在表格中,下次遇到相同子问题时不需要重复计算,而是可以直接从表格中得到该子问题的答案,从而实现降低时间复杂度的目的。
动态规划的三个要素是状态定义、边界情况和状态转移方程。首先需要定义状态的含义,然后直接得到边界情况中的每个状态的结果,最后使用状态转移方程计算所有状态的结果。
思路和算法
用 distances \textit{distances} distances 表示结果数组。对于 0 ≤ i < m 0 \le i < m 0≤i<m 和 0 ≤ j < n 0 \le j < n 0≤j<n,如果 mat [ i ] [ j ] = 0 \textit{mat}[i][j] = 0 mat[i][j]=0 则 distances [ i ] [ j ] = 0 \textit{distances}[i][j] = 0 distances[i][j]=0,如果 mat [ i ] [ j ] = 1 \textit{mat}[i][j] = 1 mat[i][j]=1 则 distances [ i ] [ j ] \textit{distances}[i][j] distances[i][j] 为矩阵的第 i i i 行第 j j j 列到最近的 0 0 0 的距离。
矩阵中的每个 1 1 1 都可以经过竖直方向移动 x x x 次和水平方向移动 y y y 次到达最近的 0 0 0(其中 x x x 和 y y y 是非负整数),竖直方向可以向上或向下移动,水平方向可以向左或向右移动。因此可以使用动态规划计算。
结果数组中的元素是动态规划的状态,动态规划的边界情况是当 mat [ i ] [ j ] = 0 \textit{mat}[i][j] = 0 mat[i][j]=0 时 distances [ i ] [ j ] = 0 \textit{distances}[i][j] = 0 distances[i][j]=0。
当 mat [ i ] [ j ] = 1 \textit{mat}[i][j] = 1 mat[i][j]=1 时,动态规划的状态转移方程如下:
distances [ i ] [ j ] = min ( distances [ i − 1 ] [ j ] , distances [ i + 1 ] [ j ] , distances [ i ] [ j − 1 ] , distances [ i ] [ j + 1 ] ) + 1 \textit{distances}[i][j] = \min(\textit{distances}[i - 1][j], \textit{distances}[i + 1][j], \textit{distances}[i][j - 1], \textit{distances}[i][j + 1]) + 1 distances[i][j]=min(distances[i−1][j],distances[i+1][j],distances[i][j−1],distances[i][j+1])+1
计算状态 distances [ i ] [ j ] \textit{distances}[i][j] distances[i][j] 时,需要考虑行下标 i i i 和列下标 j j j 分别从小到大遍历和从大到小遍历的顺序,计算如下。
-
行下标 i i i 从小到大遍历,列下标 j j j 从小到大遍历, distances [ i ] [ j ] = min ( distances [ i ] [ j ] , distances [ i − 1 ] [ j ] , distances [ i ] [ j − 1 ] ) \textit{distances}[i][j] = \min(\textit{distances}[i][j], \textit{distances}[i - 1][j], \textit{distances}[i][j - 1]) distances[i][j]=min(distances[i][j],distances[i−1][j],distances[i][j−1])。
-
行下标 i i i 从小到大遍历,列下标 j j j 从大到小遍历, distances [ i ] [ j ] = min ( distances [ i ] [ j ] , distances [ i ] [ j + 1 ] ) \textit{distances}[i][j] = \min(\textit{distances}[i][j], \textit{distances}[i][j + 1]) distances[i][j]=min(distances[i][j],distances[i][j+1])。
-
行下标 i i i 从大到小遍历,列下标 j j j 从小到大遍历, distances [ i ] [ j ] = min ( distances [ i ] [ j ] , distances [ i + 1 ] [ j ] , distances [ i ] [ j − 1 ] ) \textit{distances}[i][j] = \min(\textit{distances}[i][j], \textit{distances}[i + 1][j], \textit{distances}[i][j - 1]) distances[i][j]=min(distances[i][j],distances[i+1][j],distances[i][j−1])。
-
行下标 i i i 从大到小遍历,列下标 j j j 从大到小遍历, distances [ i ] [ j ] = min ( distances [ i ] [ j ] , distances [ i ] [ j + 1 ] ) \textit{distances}[i][j] = \min(\textit{distances}[i][j], \textit{distances}[i][j + 1]) distances[i][j]=min(distances[i][j],distances[i][j+1])。
上述计算中存在重复计算,其实只需要保留两项:行下标和列下标都从小到大遍历,以及行下标和列下标都从大到小遍历。
代码
java
class Solution {
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
static final int INFINITY = Integer.MAX_VALUE / 2;
public int[][] updateMatrix(int[][] mat) {
int m = mat.length, n = mat[0].length;
int[][] distances = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 1) {
distances[i][j] = INFINITY;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 1) {
if (i > 0) {
distances[i][j] = Math.min(distances[i][j], distances[i - 1][j] + 1);
}
if (j > 0) {
distances[i][j] = Math.min(distances[i][j], distances[i][j - 1] + 1);
}
}
}
}
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (mat[i][j] == 1) {
if (i < m - 1) {
distances[i][j] = Math.min(distances[i][j], distances[i + 1][j] + 1);
}
if (j < n - 1) {
distances[i][j] = Math.min(distances[i][j], distances[i][j + 1] + 1);
}
}
}
}
return distances;
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 mat \textit{mat} mat 的行数和列数。初始化结果数组需要 O ( m n ) O(mn) O(mn) 的时间,计算结果数组需要遍历矩阵 mat \textit{mat} mat 两次,时间是 O ( m n ) O(mn) O(mn)。
-
空间复杂度: O ( 1 ) O(1) O(1)。注意返回值不计入空间复杂度。