题目0064:最小路径和

题目描述

给定一个包含非负整数的mxn网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:

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

解题技巧

暴力就是利用递归,对于每个元素我们考虑两条路径,向右走和向下走,在这两条路径中挑选路径权值和较小的一个。

\mathrm{cost}(i, j)=\mathrm{grid}[i][j] + \min \big(\mathrm{cost}(i+1, j), \mathrm{cost}(i, j+1) \big)
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        return calculate(grid, 0, 0);
    }
    int calculate(vector<vector<int>>& grid, int i, int j) {
        if (i == grid.size() || j == grid[0].size()) return 0x3f3f3f3f;
        if (i == grid.size() - 1 && j == grid[0].size() - 1) return grid[i][j];
        return grid[i][j] + min(calculate(grid, i + 1, j), calculate(grid, i, j + 1));
    }
};

复杂度分析

时间复杂度:O\big(2^{m+n}\big)。每次移动最多可以有两种选择。

空间复杂度:O(m+n)。递归的深度是m+n。

算法:我们新建一个额外的dp数组,与原矩阵大小相同。在这个矩阵中,dp(i,j)表示从坐标(i,j)到右下角的最小路径权值。我们初始化右下角的dp值为对应的原矩阵值,然后去填整个矩阵,对于每个元素考虑移动到右边或者下面,因此获得最小路径和我们有如下递推公式:

dp(i, j)= \mathrm{grid}(i,j)+\min\big(dp(i+1,j),dp(i,j+1)\big)

注意边界情况。下图描述了这个过程:

public class Solution {
    public int minPathSum(int[][] grid) {
        int[][] dp = new int[grid.length][grid[0].length];
        for (int i = grid.length - 1; i >= 0; i--) {
            for (int j = grid[0].length - 1; j >= 0; j--) {
                if(i == grid.length - 1 && j != grid[0].length - 1)
                    dp[i][j] = grid[i][j] +  dp[i][j + 1];
                else if(j == grid[0].length - 1 && i != grid.length - 1)
                    dp[i][j] = grid[i][j] + dp[i + 1][j];
                else if(j != grid[0].length - 1 && i != grid.length - 1)
                    dp[i][j] = grid[i][j] + Math.min(dp[i + 1][j], dp[i][j + 1]);
                else
                    dp[i][j] = grid[i][j];
            }
        }
        return dp[0][0];
    }
}

复杂度分析

时间复杂度:O(mn)。遍历整个矩阵恰好一次。

空间复杂度:O(mn)。额外的一个同大小矩阵。

算法:在上个解法中,我们可以用一个一维数组来代替二维数组,dp数组的大小和行大小相同。这是因为对于某个固定状态,只需要考虑下方和右侧的节点。首先初始化dp数组最后一个元素是右下角的元素值,然后我们向左移更新每个dp(j)为:

dp(j)=\mathrm{grid}(i,j)+\min\big(dp(j),dp(j+1)\big)

我们对于每一行都重复这个过程,然后向上一行移动,计算完成后dp(0)就是最后的结果。

public class Solution {
   public int minPathSum(int[][] grid) {
       int[] dp = new int[grid[0].length];
       for (int i = grid.length - 1; i >= 0; i--) {
           for (int j = grid[0].length - 1; j >= 0; j--) {
               if(i == grid.length - 1 && j != grid[0].length - 1)
                   dp[j] = grid[i][j] +  dp[j + 1];
               else if(j == grid[0].length - 1 && i != grid.length - 1)
                   dp[j] = grid[i][j] + dp[j];
               else if(j != grid[0].length - 1 && i != grid.length - 1)
                   dp[j] = grid[i][j] + Math.min(dp[j], dp[j + 1]);
               else
                   dp[j] = grid[i][j];
           }
       }
       return dp[0];
   }
}

复杂度分析

时间复杂度:O(mn)。遍历整个矩阵恰好一次。

空间复杂度:O(n)。额外的一维数组,和一行大小相同。

算法:和方法2相同,惟一的区别是,不需要用额外的dp数组,而是在原数组上存储,这样就不需要额外的存储空间。递推公式如下:

\mathrm{grid}(i, j)=\mathrm{grid}(i,j)+\min \big(\mathrm{grid}(i+1,j), \mathrm{grid}(i,j+1)\big)
public class Solution {
    public int minPathSum(int[][] grid) {
        for (int i = grid.length - 1; i >= 0; i--) {
            for (int j = grid[0].length - 1; j >= 0; j--) {
                if(i == grid.length - 1 && j != grid[0].length - 1)
                    grid[i][j] = grid[i][j] +  grid[i][j + 1];
                else if(j == grid[0].length - 1 && i != grid.length - 1)
                    grid[i][j] = grid[i][j] + grid[i + 1][j];
                else if(j != grid[0].length - 1 && i != grid.length - 1)
                    grid[i][j] = grid[i][j] + Math.min(grid[i + 1][j],grid[i][j + 1]);
            }
        }
        return grid[0][0];
    }
}

复杂度分析

时间复杂度:O(mn)。遍历整个矩阵恰好一次。

空间复杂度:O(1)。不需要额外空间。