两道题回顾之前学习的动态规划所有流程— 康Sir的学习笔记

LeetCode931 最小下降路径和

输入为一个 n * n 的二维数组 matrix,请你计算从第一行落到最后一行,经过的路径和最小为多少。

  • 最初动态规划代码,暴力规划
int minFallingPathSum(vector<vector<int>>& matrix) {
    int n = matrix.size();
    //dp数组定义: dp[i][j]表示下降到位置(i,j)时的最小路径和
    std::vector<vector<int>> dp(n,std::vector<int>(n,0));
    //base case: 第一行是初始位置,需要在状态转移开始前对其赋值
    for(int j=0;j<n;j++){
        dp[0][j] = matrix[0][j];
    }

    //开始状态转移
    for(int i=1;i<n;i++){
        for(int j=0;j<n;j++){
            //第一列
            if(j == 0){
                dp[i][j] = std::min(dp[i-1][j],dp[i-1][j+1]) + matrix[i][j];
            }
            //最后一列
            else if(j == n-1){
                dp[i][j] = std::min(dp[i-1][j-1],dp[i-1][j]) + matrix[i][j];
            }
            else{
                dp[i][j] = std::min(dp[i-1][j-1],std::min(dp[i-1][j],dp[i-1][j+1])) + matrix[i][j];
            }
        }
    }

    int min = dp[n-1][0];
    for(int i=1;i<n;i++){
        if(dp[n-1][i] < min){
            min = dp[n-1][i];
        }
    }
    return min;
}
时间复杂度方面

由于利用的是dp数组,故无需使用备忘录来保存递归结果(利用递归解决子问题时的概念)。

空间复杂度方面

我们观察到,dp[i][j] 的值可以由dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1] 计算出来,所以压缩之后的一维dp 需要具有通过某种方式得出 dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1] 的能力,我们先直接去除 i 这个维度。

去除i后的状态转移:

//开始状态转移
for(int i=1;i<n;i++){
    for(int j=0;j<n;j++){
        //第一列
        if(j == 0){
            dp[j] = std::min(dp[j],dp[j+1]) + matrix[i][j];
        }
        //最后一列
        else if(j == n-1){
            dpj] = std::min(dp[j-1],dp[j]) + matrix[i][j];
        }
        else{
            dp[j] = std::min(dp[j-1],std::min(dp[j],dp[j+1])) + matrix[i][j];
        }
    }
}

重点分析,(其实想明白下面几个问题,压缩之后的状态转移就好写出来了):

  1. dp[j] 未进行更新前, dp[j]表示什么?
  2. dp[j-1] 表示什么?
  3. dp[j+1] 表示什么?

dp[j] 未更新前,dp[j]表示的是上一层外循环的dp[i-1][j]dp[j+1] 表示的是上一层外循环的dp[i-1][j+1],而 dp[j-1]表示的是上一层内循环的dp[i][j-1]

分析到现在,只有dp[i-1][j-1] 不能得出,但是站在循环到dp[i][j]的角度上想, dp[i-1][j-1] 不就是被在dp[j-1]位置被dp[i][j-1]覆盖掉的元素吗,我们新建一个临时变量,来保存这个被覆盖的值就可以了。

//开始状态转移
for(int i=1;i<n;i++){
    int pre = 0;
    for(int j=0;j<n;j++){
        int temp = dp[j];
        //第一列
        if(j == 0){
            dp[j] = std::min(dp[j],dp[j+1]) + matrix[i][j];
        }
        //最后一列
        else if(j == n-1){
            dpj] = std::min(pre,dp[j]) + matrix[i][j];
        }
        else{
            dp[j] = std::min(pre,std::min(dp[j],dp[j+1])) + matrix[i][j];
        }
        pre = temp;
    }
}

现在状态转移已经全部得出来了,接下只要再分析 一维dp 对应的base case 即可。

回顾下 二维dpbase case

//base case: 第一行是初始位置,需要在状态转移开始前对其赋值
for(int j=0;j<n;j++){
    dp[0][j] = matrix[0][j];
}

二维dpbase case 其实就是单独把第一行拎出来分析,我们直接投影到第一行就行。

//base case: 第一行是初始位置,需要在状态转移开始前对其赋值
for(int j=0;j<n;j++){
    dp[j] = matrix[0][j];
}
  • 空间压缩后的最终代码:
int minFallingPathSum(vector<vector<int>>& matrix) {
    int n = matrix.size();
    int dp[n];
    //base case: 第一行是初始位置,需要在状态转移开始前对其赋值
    for(int j=0;j<n;j++){
    	dp[j] = matrix[0][j];
	}

   //开始状态转移
    for(int i=1;i<n;i++){
        int pre = 0;
        for(int j=0;j<n;j++){
            int temp = dp[j];
            //第一列
            if(j == 0){
                dp[j] = std::min(dp[j],dp[j+1]) + matrix[i][j];
            }
            //最后一列
            else if(j == n-1){
                dpj] = std::min(pre,dp[j]) + matrix[i][j];
            }
            else{
                dp[j] = std::min(pre,std::min(dp[j],dp[j+1])) + matrix[i][j];
            }
            pre = temp;
        }
    }

    int min = dp[0];
    for(int i=1;i<n;i++){
        if(dp[i] < min){
            min = dp[i];
        }
    }
    return min;
}
LeetCode174 地下城游戏(从终点开始分析状态转移)
错误的dp数组定义和我的错误思考

和之前的动态规划套路一样,我们定义dp数组,考虑清楚状态转移方程,写明 base case 并最终返回转移到目标状态的dp即可。

  • 我是这样定义dp数组的:
dp[i][j]代表走到位置(i,j)时需要的最低初始健康点数

接下来考虑状态转移方程时,发现会有问题,dp[i][j] 并不能由相邻的dp[][] 表示出来。因为dp[i][j]并不只跟dp[i][j-1],dp[i-1][j]有关系,因为最低初始点数是骑士在这段路径上的数字和的最小值的绝对值,dp[i][j-1] 只能表示骑士在走到位置(i,j-1)这段路程内数字和的那个最小值,并不是以(i,j)为终点的这段路径上的数字和。

看我的下面一段递归思路分析就会明白上面这段话到底是什么意思了:

[!TIP]

因为dp[i][j]并不只跟dp[i][j-1],dp[i-1][j]有关系。但是,我们知道,最低初始健康点数,就是在走这条路径时,期间血量达到最低的时刻的那个最低值的绝对值+ 1嘛。(如果这个minSum为负,则取绝对值再加上1,如果这个minSum为正,那么骑士就算一开始一点血都没有(为0)都可以成功走完这条路径救到公主,但是初始值最少为1吗,如果minSum为正,那么初始血量为1就行了)

怎么用代码展示出来呢?
如果能有两个变量跟着骑士,随着骑士移动而变化就好了,HP用来记录此刻骑士血量,minSum用来记录血量变化中的最小值,这样我们只需要随时更新这个最小值,当骑士走到(i,j)时,取这个minSum对应的最小初始值就好。

递归如何保证骑士走到右下角呢?
如何从每个到达右下角的路径中去对应血量初始值的最小值呢?
这种思路是递归的思路,现在暂时还解决不了(至少我现在解不出来,hhh...)

其实我的思路就是把这个正方形旋转成菱形,(0,0)是根结点,(i-1,j-1)为最终层。

!!!这段递归思路分析不必细看,这只是我对这个问题的另一种思考,这个思路并不能解决问题,但是我们能从中了解到最低初始点数是怎么来的。

从终点开始分析,得出dp数组定义和状态转移方程

现在我们重新思考一下dp数组该怎么定义,既然从起点开始分析问题不能解决问题,我们试试从终点开始分析:

如果骑士一开始就在终点,即处于(row-1,column-1)位置时,所需要的最低初始点数只要比当前位置伤害的绝对值大1即可。

  • 我们这样定义dp 数组:
dp[i][j] 表示以[i][j]位置为起点,移动到右下角所需要的最低初始点数。
// base case 为骑士从终点开始移动到终点
dp[row-1][column-1] = M[i][j] > 0 ? 1 : abs(M[i][j]) + 1;
  • 状态转移方程:
dp[i][j] = min(dp[i+1][j],dp[i][j+1]) - M[i][j] <= 0 ? 1 : min(dp[i+1][j],dp[i][j+1]) - M[i][j]  ;

当然。这个状态转移方程没有考虑边界情况。边界情况我们直接在代码中体现:

int calculateMinimumHP(vector <vector<int>> &dungeon) {
    //定义dp数组,dp[i][j] 表示以[i][j]位置为起点,移动到右下角所需要的最低初始点数。
    int row = dungeon.size();
    int column = dungeon[0].size();
    int dp[row][column];
    //base case
    dp[row - 1][column - 1] = dungeon[row - 1][column - 1] >= 0 ? 1 : std::abs(dungeon[row - 1][column - 1]) + 1;

    //状态转移
    for (int i = row - 1; i >= 0; i--) {
        for (int j = column - 1; j >= 0; j--) {
            if (i == row - 1 && j == column - 1) continue;
            if (i == row - 1) {
                dp[i][j] = dp[i][j + 1] - dungeon[i][j] <= 0 ? 1 : dp[i][j + 1] - dungeon[i][j];
            } else if (j == column - 1) {
                dp[i][j] = dp[i + 1][j] - dungeon[i][j] <= 0 ? 1 : dp[i + 1][j] - dungeon[i][j];
            } else {
                dp[i][j] = std::min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j] <= 0 ? 1 :
                           std::min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
            }
        }
    }

    return dp[0][0];
}    

这题还可以继续空间压缩,读者可参照之前的空间压缩文章 和上一题 自行尝试一下压缩。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

fresher.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值