剑指 Offer II 107. 矩阵中的距离-一题多解BFS + 动态规划DP(Java)

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的运行结果 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值