【Leetcode每日一刷】动态规划:931. 下降路径最小和

在这里插入图片描述

在这里插入图片描述

一、动态规划套路

动规五部曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式(状态转移方程)
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

二、分析

就是说你可以站在 matrix 的第一行的任意一个元素,需要下降到最后一行。

每次下降,可以向下、向左下、向右下三个方向移动一格。也就是说,可以从 matrix[i][j] 降到 matrix[i+1][j]matrix[i+1][j-1]matrix[i+1][j+1] 三个位置。

1、dp数组含义

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

int dp(vector<vector<int>>& matrix, int i, int j);

写出主体逻辑代码:

int minFallingPathSum(vector<vector<int>>& matrix) {
    int n = matrix.size();
    int res = INT_MAX;

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

    return res;
}

2、确定递推公式(递推函数的实现)

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

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

int dp(vector<vector<int>>& matrix, int i, int j) {
    // 非法索引检查
    if (i < 0 || j < 0 ||
        i >= matrix.size() ||
        j >= matrix[0].size()) {
        // 返回一个特殊值
        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 min(a, min(b, c));
}

3、dp数组初始化(base case)

	int dp(vector <vector <int>> & matrix, vector<vector<int>>& memo, int i, int j){
		//base case 
		if(i == 0){
			 return matrix[0][j];
        }
	}

4、遍历顺序

初始化初始化第一行,那么根据二维数组,以及确定的data base的位置和最终状态,确定:从上往下遍历

   // 进行状态转移
        memo[i][j] = matrix[i][j] + min(
                dp(matrix, memo, i - 1, j), 
                dp(matrix, memo, i - 1, j - 1),
                dp(matrix, memo, i - 1, j + 1)
            );

解题(代码):


int dp(vector<vector<int>>& matrix, int i, int j) {
    // 非法索引检查
    if (i < 0 || j < 0 ||
        i >= matrix.size() ||
        j >= matrix[0].size()) {
        // 返回一个特殊值
        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 min(a, min(b, c));
}

上面代码能够暴力递归解决,但是有很多***重叠子问题***,降低了时间效率。

可以采用备忘录,记录下来已经确定的状态,不用每次去递归算。来消除重叠子问题:


class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n = matrix.size();
        int res = INT_MAX;
        // 备忘录里的值初始化为 66666
        vector<vector<int>> memo(n, vector<int>(n, 66666));
        // 终点可能在 matrix[n-1] 的任意一列
        for (int j = 0; j < n; j++) {
            res = min(res, dp(matrix, memo, n - 1, j));
        }
        return res;
    }

    int dp(vector<vector<int>>& matrix, vector<vector<int>>& memo, int i, int j) {
        // 1、索引合法性检查
        if (i < 0 || j < 0 ||
            i >= matrix.size() ||
            j >= matrix[0].size()) {
            
            return 99999;
        }
        // 2、base case
        if (i == 0) {
            return matrix[0][j];
        }
        // 3、查找备忘录,防止重复计算
        if (memo[i][j] != 66666) {
            return memo[i][j];
        }
        // 进行状态转移
        memo[i][j] = matrix[i][j] + min(
                dp(matrix, memo, i - 1, j), 
                dp(matrix, memo, i - 1, j - 1),
                dp(matrix, memo, i - 1, j + 1)
            );
        return memo[i][j];
    }

    int min(int a, int b, int c) {
        return min(a, min(b, c));
    }
};
  • memo的初始值为什么是66666:

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

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

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

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

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

matrix 是 n x 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) 中就可以。

  • 索引合法性检测:99999:
    • 对于不合法的索引,返回值如何确定,要看状态转移方程的逻辑
    • 状态转移方程如下:
int dp(vector <vector<int>> & matrix, int i, int j) {
	return matrix[i][j] + min(
		dp(matrix, memo, i-1,j),
		dp(matrix, memo, i-1,j-1)
		dp(matrix, memo, i-1,j+1)
	);
}

通过状态方程可知,如果索引不合法,是取该状态+前三个状态的最小值。在i-1j+1中,均有可能出现不合法的情况。

只需让其返回一个特殊的大值,让min取不到它即可。

通过上面分析,该题的最终状态结构范围在[-10000, 10000] 中。所以只需返回>10000的值即可。更准确来说的只要返回区间 [10001, +inf) 中的一个值,就能保证不会被取到

三、最终完整代码:

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {

        int n = matrix.size();
        //防止重复计算,创建一个备忘录数组
        vector < vector<int> > memo(n, vector<int>(n,55555));//初始化备忘录数组

        //主体逻辑代码:(遍历最后一行,终点可能在最后一行的任何一列)

        int ans = INT_MAX;
        for (int i = 0; i < matrix.size(); i++){
           ans = min(ans,dp(matrix,memo,n-1,i));
        }
        return ans;
    }

    int dp(vector<vector<int>> &matrix, vector< vector<int> > &memo, int i,int j){
        //1)首先要检测下标是否合法
        if(i<0||j<0||i>=matrix.size()||j>=matrix.size()){
            return 55555;
        }
        //2)初始化dp数组
        if(i==0){
            return matrix[i][j];
        }

        //备忘录查找防止重复
        if(memo[i][j]!=55555){
            return memo[i][j];
        }

        //3)状态转移方程
        //不要忘记装进备忘录里哦
        memo[i][j] = matrix[i][j] + min_(
            dp(matrix,memo,i-1,j),
            dp(matrix,memo,i-1,j-1),
            dp(matrix,memo,i-1,j+1)
        );
        return memo[i][j];
    }
    int min_(int a, int b, int c){
        return min (c,min(a,b));
    }
};


欢迎在评论区交流和留下你的想法和建议

如果对你有用,还请:💭评论+👍🏻点赞+⭐收藏+➕关注

在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是瑶瑶子啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值