【Leetcode每日一刷】动态规划:931. 下降路径最小和
- 博主简介:努力学习的22级计科生
- 博主主页: @是瑶瑶子啦
- 所属专栏: LeetCode每日一题–进击大厂
目录
- 一、动态规划套路
- 二、分析
- 1、dp数组含义
- 2、确定递推公式(递推函数的实现)
- 3、dp数组初始化(base case)
- 4、遍历顺序
- 解题(代码):
- 三、最终完整代码:
一、动态规划套路
动规五部曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式(状态转移方程)
- dp数组如何初始化
- 确定遍历顺序
- 举例推导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-1
和j+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));
}
};
欢迎在评论区交流和留下你的想法和建议
如果对你有用,还请:💭评论+👍🏻点赞+⭐收藏+➕关注
- Java岛冒险记【从小白到大佬之路】
- LeetCode每日一题–进击大厂
- 算法
- C/C++
- Go语言核心编程
- 数据结构