931. 下降路径最小和

144 阅读4分钟

题目介绍

力扣931题:leetcode-cn.com/problems/mi…

image.png

动态规划

首先我们可以定义一个dp数组:

int dp(int[][] matrix, int i, int j);

这个dp函数的含义如下:

从第一行(matrix[0][..])向下落,落到位置matrix[i][j]的最小路径和为dp(matrix, i, j)

根据这个定义,我们可以把主函数的逻辑写出来:

public int minFallingPathSum(int[][] matrix) {
    int n = matrix.length;
    int res = Integer.MAX_VALUE;

    // 终点可能在最后一行的任意一列
    for (int j = 0; j < n; j++) {
        res = Math.min(res, dp(matrix, n - 1, j));
    }

    return res;
}

因为我们可能落到最后一行的任意一列,所以要穷举一下,看看落到哪一列才能得到最小的路径和。

接下来看看dp函数如何实现。

对于matrix[i][j],只有可能从matrix[i-1][j], matrix[i-1][j-1], matrix[i-1][j+1]这三个位置转移过来。

image.png

那么,只要知道到达(i-1, j), (i-1, j-1), (i-1, j+1)这三个位置的最小路径和,加上matrix[i][j]的值,就能够计算出来到达位置(i, j)的最小路径和

int dp(int[][] matrix, int i, int j) {
    // 非法索引检查
    if (i < 0 || j < 0 ||
        i >= matrix.length ||
        j >= matrix[0].length) {
        // 返回一个特殊值
        return 99999;
    }
    // base case
    if (i == 0) {
        return matrix[i][j];
    }
    // 状态转移
    return matrix[i][j] + min(
            dp(matrix, i - 1, j), 
            dp(matrix, i - 1, j - 1),
            dp(matrix, i - 1, j + 1)
        );
}

int min(int a, int b, int c) {
    return Math.min(a, Math.min(b, c));
}

当然,上述代码是暴力穷举解法,我们可以用备忘录的方法消除重叠子问题,完整代码如下:

class Solution {
    int[][] memo;
    public int minFallingPathSum(int[][] matrix) {
        int n = matrix.length;
        int res = Integer.MAX_VALUE;
        //备忘录
        memo = new int[n][n];
        for(int i = 0; i < n; i++) {
            Arrays.fill(memo[i], 66666);
        }
        //终点肯定是在最后一行,但是具体哪一列需要比较
        for(int j = 0; j < n; j++) {
            res = Math.min(res, dp(matrix, n-1, j));
        }
        return res;
    }

    int dp(int[][] matrix, int i, int j) {
        //索引合法性检查
        if(i < 0 || j < 0 || i >= matrix.length || j >= matrix[0].length) {
            return 99999;
        }
        //base case
        if(i == 0) {
            return matrix[0][j];
        }
        //查找备忘录,防止重复计算
        if(memo[i][j] != 66666) {
            return memo[i][j];
        }
        //进行状态转移
        memo[i][j] = matrix[i][j] + min(
            dp(matrix, i-1, j),
            dp(matrix, i-1, j-1),
            dp(matrix, i-1, j+1)
            );
        return memo[i][j];
    }
    int min(int a, int b, int c) {
        return Math.min(a, Math.min(b, c));
    }
}
  • 1、对于索引的合法性检测,返回值为什么是 99999?其他的值行不行?

  • 2、base case 为什么是i == 0

  • 3、备忘录memo的初始值为什么是 66666?其他值行不行?

首先,说说 base case 为什么是i == 0,返回值为什么是matrix[0][j],这是根据dp函数的定义所决定的

回顾我们的dp函数定义:

从第一行(matrix[0][..])向下落,落到位置matrix[i][j]的最小路径和为dp(matrix, i, j)

根据这个定义,我们就是从matrix[0][j]开始下落。那如果我们想落到的目的地就是i == 0,所需的路径和当然就是matrix[0][j]呗。

再说说备忘录memo的初始值为什么是 66666,这是由题目给出的数据范围决定的

备忘录memo数组的作用是什么?

就是防止重复计算,将dp(matrix, i, j)的计算结果存进memo[i][j],遇到重复计算可以直接返回。

那么,我们必须要知道memo[i][j]到底存储计算结果没有,对吧?如果存结果了,就直接返回;没存,就去递归计算。

所以,memo的初始值一定得是特殊值,和合法的答案有所区分。

我们回过头看看题目给出的数据范围:

matrixn * n的二维数组,其中1 <= n <= 100;对于二维数组中的元素,有-100 <= matrix[i][j] <= 100

假设matrix的大小是 100 x 100,所有元素都是 100,那么从第一行往下落,得到的路径和就是 100 x 100 = 10000,也就是最大的合法答案。

类似的,依然假设matrix的大小是 100 x 100,所有元素是 -100,那么从第一行往下落,就得到了最小的合法答案 -100 x 100 = -10000。

也就是说,这个问题的合法结果会落在区间[-10000, 10000]中。

所以,我们memo的初始值就要避开区间[-10000, 10000],换句话说,memo的初始值只要在区间(-inf, -10001] U [10001, +inf)中就可以。

最后,说说对于不合法的索引,返回值应该如何确定,这需要根据我们状态转移方程的逻辑确定

对于这道题,状态转移的基本逻辑如下:

int dp(int[][] matrix, int i, int j) {

    return matrix[i][j] + min(
            dp(matrix, i - 1, j), 
            dp(matrix, i - 1, j - 1),
            dp(matrix, i - 1, j + 1)
        );
}

显然,i - 1, j - 1, j + 1这几个运算可能会造成索引越界,对于索引越界的dp函数,应该返回一个不可能被取到的值。

因为我们调用的是min函数,最终返回的值是最小值,所以对于不合法的索引,只要dp函数返回一个永远不会被取到的最大值即可。

刚才说了,合法答案的区间是[-10000, 10000],所以我们的返回值只要大于 10000 就相当于一个永不会取到的最大值。

换句话说,只要返回区间[10001, +inf)中的一个值,就能保证不会被取到。

至此,我们就把动态规划相关的三个细节问题举例说明了。