1.题目
2.思路
看见这题,第一反应就是BFS。不过这里比较特殊的是,起点会有多个,也就是需要判断每个0对所有的1 的距离,从每一个0开始BFS, 每一轮BFS都需要对所有的1进行迭代距离。这样子最坏的时间复杂度为,首先会遍历n * m个 0, 然后每一轮BFS的时间复杂度为O(n * m),所以最终的时间复杂度为O(n * m * n * m),所以会超时!!!
比如最开始写的超时代码:(通过用例49 / 49, 但是就是显示超时)
时间复杂度——O(N * M * N * M)
空间复杂度——O(N * M)
class Solution {
public int[][] updateMatrix(int[][] mat) {
// BFS
int n = mat.length;
int m = mat[0].length;
int[][]ans = new int[n][m];
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < m ; j++){
if(mat[i][j] == 1){
int[][]vis = new int[n][m];
Deque<int[]>d = new ArrayDeque();
d.addLast(new int[]{i, j, 0});
int tempAns = Integer.MAX_VALUE;
while(!d.isEmpty()){
int[]cur = d.pollFirst();
int x = cur[0], y = cur[1], step = cur[2];
if(x < 0 || x >= n || y < 0 || y >= m || vis[x][y] == 1)continue;
vis[x][y] = 1;
if(mat[x][y] == 0){
tempAns = step;
break;
}
int[]dx = new int[]{0, 0, 1, -1};
int[]dy = new int[]{1, -1, 0, 0};
for(int k = 0 ; k < 4 ; k++){
d.addLast(new int[]{x + dx[k], y + dy[k], step + 1});
}
}
ans[i][j] = tempAns;
}
}
}
return ans;
}
}
那么应该如何避免呢?--采取多源BFS
方法1——多源BFS(等价于超级源点的BFS)
多源BFS 比单源BFS 时间更快!来看一个多源BFS的图,来源力扣题解官方
多源BFS,其实就是等价于一个假想的超级源点S,这个S 与所有的0都有一个相连的边。此时我们要求解的问题由原来的“找到离1最近的0的距离” 转化为了“找到1与超级源点S的距离”(因为这里定义为超级源点与0相连距离为0)所以直接进行一次超级源点的BFS 即可。
那么具体在代码中如何写呢?(比较类似二叉树的层序遍历)
第一层,相当于把所有的0都加入了队列。
第二层,相当于把所有与0直接相连的1加入队列。
第三层,相当于把与第二层的1直接相连的1加入队列,依此类推。注意用一个数组int[][]vis记录该值是否被访问过。
时间复杂度——O(N * M)
找到初始的0需要时间O(N * M), 然后在单独的队列中,时间复杂度也是O(N * M),所以最终还是O(N * M)
空间复杂度——O(N * M)
使用了额外数组,用一个int[][]vis来记录是否访问过。所以空间O(N * M)
class Solution {
public int[][] updateMatrix(int[][] mat) {
int n = mat.length;
int m = mat[0].length;
int[][]ans = new int[n][m];
int[][]vis = new int[n][m];
Deque<int[]>d = new ArrayDeque();
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < m ; j++){
if(mat[i][j] == 0){
vis[i][j] = 1;
d.addLast(new int[]{i, j});
}
}
}
while(!d.isEmpty()){
int[]cur = d.pollFirst();
int x = cur[0], y = cur[1];
int[]dx = new int[]{0, 0, 1, -1};
int[]dy = new int[]{1, -1, 0, 0};
for(int i = 0 ; i < 4; i++){
int nx = x + dx[i], ny = y + dy[i];
if(nx >= 0 && nx < n && ny >= 0 && ny < m && vis[nx][ny] == 0){
ans[nx][ny] = ans[x][y] + 1;
vis[nx][ny] = 1;
d.addLast(new int[]{nx, ny});
}
}
}
return ans;
}
}
方法2——四个方向的动态规划dp
这道题还能用动态规划解决,一开始压根没往这方面思考。不过一般求最小距离的确实很容易是动态规划。
动态规划三部曲
1.定义dp[i][j]:点(i, j)到最近的0之间的距离为dp[i][j].
2.初始化:第一步先全部赋值一个很大的数(比如N * M).第二步让mat[i][j] = 0的点的距离为0.
3.转移方程:这个比较难想。
转移方程分成了四个方向来思考。当最近的0在点(i, j)的左上方时候, 当最近的点0在点(i, j)的左下方的时候,当最近的0在点(i, j)右上方的时候,当最近的点0在点(i, j)的右下方的时候。
举个例子,比如是左上方。
此时dp[i][j]要么等于上一行同一列的dp[i-1][j] + 1, 要么等于同一行左边一列dp[i][j -1] + 1.取其中的最小值即可,注意不要越界。
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
同理,如果是左下方呢?(注意这里的i, j大小)
dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
如果是右上,右下,也一样。
最终,经过四次dp后,可以得到最终的答案。
时间复杂度——O(N * M)
因为是二维dp, 四次O(N * M)相加,最终也是O(N * M)
空间复杂度——O(1)
这里没有使用额外的数据、队列、栈之类的,所以没有空间消耗。dp数组本身就是要返回的数组,不算开销。
class Solution {
public int[][] updateMatrix(int[][] mat) {
int n = mat.length;
int m = mat[0].length;
// dp[i][j] : (i, j)到最近的0的距离
// 可以分成四个部分,最近的0在左上。最近的0在左下。最近的0在右上。最近的0在右下。
int[][]dp = new int[n][m];
// 初始化
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < m; j++){
if(mat[i][j] == 0)
dp[i][j] = 0;
else
dp[i][j] = n * m; //因为这里需要对原来的值 + 1, 所以不能定义为最大的整数值,不然会溢出
}
}
// 1.最近的0在左上的情况
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < m ; j++){
if(i >= 1)
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
if(j >= 1)
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
}
}
// 2.左下, 注意此时遍历的顺序,因为此时最近的0在(i, j)的左下方,所以必须从最左下方开始遍历
for(int i = n - 1 ; i >= 0 ; i--){
for(int j = 0 ; j < m ; j++){
if(i + 1 < n)
dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
if(j - 1 >= 0)
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
}
}
// 3.右上
for(int i = 0 ; i < n ; i++){
for(int j = m -1 ; j >= 0 ; j--){
if(j + 1 < m)
dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1);
if(i - 1 >= 0)
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
}
}
// 4.右下
for(int i = n - 1 ; i >= 0 ; i--){
for(int j = m - 1 ; j >= 0 ; j--){
if(j + 1 < m)
dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1);
if(i + 1 < n)
dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
}
}
return dp;
}
}
后面还有进一步的优化,可以将四个方向的dp只保留两个方向,那就是对角线左上 和 右下。
class Solution {
public int[][] updateMatrix(int[][] mat) {
int n = mat.length;
int m = mat[0].length;
int[][]ans = new int[n][m];
// 记录是否被访问过
int[][]vis = new int[n][m];
// dp[i][j] : (i, j)到最近的0的距离
// 可以分成四个部分,最近的0在左上。最近的0在左下。最近的0在右上。最近的0在右下。
int[][]dp = new int[n][m];
// 初始化
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < m; j++){
if(mat[i][j] == 0)
dp[i][j] = 0;
else
dp[i][j] = n * m; //因为这里需要对原来的值 + 1, 所以不能定义为最大的整数值,不然会溢出
}
}
// 1.最近的0在左上的情况
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < m ; j++){
if(i >= 1)
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
if(j >= 1)
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
}
}
// // 2.左下, 注意此时遍历的顺序,因为此时最近的0在(i, j)的左下方,所以必须从最左下方开始遍历
// for(int i = n - 1 ; i >= 0 ; i--){
// for(int j = 0 ; j < m ; j++){
// if(i + 1 < n)
// dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
// if(j - 1 >= 0)
// dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
// }
// }
// // 3.右上
// for(int i = 0 ; i < n ; i++){
// for(int j = m -1 ; j >= 0 ; j--){
// if(j + 1 < m)
// dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1);
// if(i - 1 >= 0)
// dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
// }
// }
// 4.右下
for(int i = n - 1 ; i >= 0 ; i--){
for(int j = m - 1 ; j >= 0 ; j--){
if(j + 1 < m)
dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1);
if(i + 1 < n)
dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
}
}
return dp;
}
}
3.结果
最后两个方向的dp的运行结果