矩阵最小路径和的逐步优化

766 阅读4分钟

题目描述

力扣64题,给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。每次只能向下或者向右移动一步。

示例 1:

输入: grid = [[1,3,1],[1,5,1],[4,2,1]]
输出: 7
解释: 因为路径 13111 的总和最小。

原题戳这里

深度优先搜索

这道题一上来我是用深度优先遍历做的,还做了剪枝,结果果断超时

void dfs(int row,int line,int rowMax,int lineMax,int sum,int[][] grid){
        if(sum>min)
            return;
        if(row>rowMax-1||line>lineMax-1)
            return;
        if(row==rowMax-1&&line==lineMax-1){
            sum += grid[row][line];
            max = Math.min(max,sum);
            sum -= grid[row][line];
            return;
        }
        sum += grid[row][line];
        dfs(row+1,line,rowMax,lineMax,sum,grid);
        dfs(row,line+1,rowMax,lineMax,sum,grid);
        sum -= grid[row][line];
    }

这是dfs的关键代码,主函数代码我就不写了,就是每次向下或向右走一步,然后每次遍历先检查sum是否大于min,如果大于,没必要再遍历了,直接return即可,然后每次遍历到最后,对min进行更新。结果是无情超时。。。。

二维dp数组的动态规划

接着看了题解,发现可以用动态规划。这里可以用动态规划的关键就是求的是最小值,并且每次只能向下或向右移动,这样每到一个位置的最小和就只和它上边或左边的元素决定,因为只有从这两个元素的位置才能到达当前位置。由于这种性质,我们就可以利用动态规划了,假设结果数组是dp,每次遍历到一个位置i,j,此位置的最小和就是

dp[i][j] + Math.min(dp[i-1][j],dp[i][j-1]);

当然,i和j为0,也就是第一行和第一列的位置需要单独讨论,因为他们没有上边或者左边的元素。

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        for (int j = 1; j < n; j++) {
            dp[0][j] = dp[0][j - 1] + grid[0][j];
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[m - 1][n - 1];
    }
}

那么代码就可以像上面这么写了,题目也说了m,n在1到200之间,也不用写边界条件了

一维dp数组的动态规划

但是这样感觉是不是用的空间有点多,咱们其实只需要一个最后的dp[m-1][n-1],然后每个位置的值其实只和它的左边和上边的元素有关,并且二维的dp数组,剩下的那么多行,最后也只需要最后一行,之前那些行用完就没什么用了,那么基于这种思想,我们可以只用一个一维的dp数组

    int m = grid.length;
    int n = grid[0].length;
    int[] res = new int[n]; //一维滚动数组的优化的动态规划
    res[0] = grid[0][0];
    for(int i=1;i<n;++i)
        res[i] = res[i-1] + grid[0][i];
    for(int i=1;i<m;++i){
        res[0] += grid[i][0];
        for(int j=1;j<n;++j){
            res[j] = grid[i][j] + Math.min(res[j],res[j-1]);
        }
    }
    return res[n-1];

关键代码是这里

res[j] = grid[i][j] + Math.min(res[j],res[j-1]);

每次一行一行进行更新,每次对位置更新时,式子右边的res[j]可以理解为此元素的上边元素的最小路径和(由于此时res[j]还没有更新),res[j-1]就是此元素左边元素的最小路径和,再用如上代码进行更新即可。

不用额外空间的动态规划

我们观察上面的代码可以发现,grid矩阵里的每个元素也只被调用了一次(第一行和第一列的元素会被调用两次),加上题目有没有说原矩阵不能被修改,所以我们可以直接在原矩阵上进行操作,先把第一行和第一列的元素进行修改,再进行其它元素的修改,代码如下:

    int m = grid.length;
    int n = grid[0].length;
    for(int i=1;i<n;++i)
        grid[0][i] += grid[0][i-1];
    for(int i=1;i<m;++i){
        grid[i][0] += grid[i-1][0];
        for(int j=1;j<n;++j){
            grid[i][j] = grid[i][j] + Math.min(grid[i-1][j],grid[i][j-1]);
        }
    }
    return grid[m-1][n-1];

直接在原矩阵上进行修改,修改思路同dp数组是二维的情况,可以这么做,主要是因为我们只需要最后的值,原矩阵面目全非也无所谓。以及grid矩阵里每个值其实用了一次之后就不会再用到了,所以改了也没事。