套用自己的动态规划套路1 leetcode 174地下城游戏

https://leetcode-cn.com/problems/dungeon-game/

题目给我们一个矩阵m * n,骑士在右上角,公主在左下角,让我们只能在向右或向下的条件下,寻找最好的、最安全的路径。

何为最安全呢?就是这条路径上,骑士的最低健康指数比其他路径都要高。

套路的基础是首先你要对问题讨价还价。第一步就是在”讨价还价“

第一步,在原问题的基础上缩小问题规模,并得到base case

什么叫做在原问题的基础上缩小规模呢?就是你缩小规模的那些问题必须是原问题的子问题。就好比让你求"abcbda"的最大回文子串,“abc”,"bcb"都是所谓的“在原问题基础上缩小问题规模“,但是”ecd“不是,它不被"abcbda"包含嘛。

这里有两种方法可以缩小问题规模,一个是固定骑士的位置,一个是固定公主的位置,并且有一种是行不通的。固定公主是可行的,我们先用这个方法。

拿样例举例。一个3 * 3的矩阵,骑士在左上角,公主在右下角。这个问题也太麻烦了,如果骑士不在左上角(0,0)处,而是在(1,1)处多好啊,这样就是解决一个2 * 2的迷宫,要比3 * 3好多了……骑士在(1,2),(0,2),(2,2)也可以啊,反正都比原问题简单,并且在(2,2)是最简单的情况,也就是base case。(这就是一个跟问题讨价还价的过程)

第二步,确定状态和选择

我们如何在原问题的基础上表达这些小规模的问题?矩阵问题通常是设两个两个指针i、j,(i,j)表示要求骑士在(i,j)位置时救到公主所耗的最少体力。这样每一个i、j都能表示一个子问题。

那么状态就是i、j的位置。

选择是什么?假设骑士在(i、j)位置上,他为了救公主会怎么做?向右或向下移动。

第三步,确定dp数组和状态转移方程

dp数组的参数很好确定,就是i、j。dp[i][j]的值是什么?往往就是题目要求的结果,在这里是骑士从这个位置开始,为了救公主,走最安全路径途中的最低健康指数。

状态转移方程呢?确定状态转移方程需要死卡dp数组的定义,并且假设比问题规模小的那些子问题已知。也就是说,如果我们要求dp[i][j],那么比状态i、j小的子问题都已知。

先附上状态转移的代码:

dp[i][j] = min(dungeon[i][j], dp[i + 1][j] + dungeon[i][j], dp[i][j + 1]) + dungeon[i][k] 

什么意思呢?骑士在i、j位置可以向右走或者向下走。如果骑士选择向右走,那么骑士就来到了(i , j + 1)位置,并且我们已经知道了骑士初始在(i、j + 1)的位置上,走最安全路径途中的最低健康指数。看一下定义,我们从定义中可以知道,骑士如果只能向右走,那么所走的最安全的路径就是初始在(i、j + 1)的位置上,走的最安全路径。并且在这个路径上,骑士的健康指数都是在(i、j + 1)的基础上,加上dungeon[i][j]。

所以最低健康指数就是dp[i][[j + 1] + dungeon[i][j]吗?不一定。dungeon[i][j]也可能是最低的嘛。

向下同理,因此我们得到在不同选择下的dp[i][j]为
dp[i][j] = min(dungeon[i][j], dp[i + 1][j] + dungeon[i][j], dp[i][j + 1]) + dungeon[i][k]

第四步 确定遍历顺序

怎么遍历呢?这一步还是挺简单的。其实就是让你设计一个遍历方法,如果来到了状态i、j,要保证状态i + 1, j和状态i, j + 1都已知嘛。这是一个自底向下的过程,画一个二维表就可以解决。遍历顺序是

for (int j = n - 1; j >= 0; j --) for (int i = m - 1; i >= 0; i --)
在图上一画就明白了。

附上代码:

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m = dungeon.size();
        int n = dungeon[0].size();
        int dp[m + 2][n + 2];           
        dp[m - 1][n - 1] = dungeon[m - 1][n - 1];
        int res = 1;
        for (int j = n - 1; j >= 0; j --) {
            for (int i = m - 1; i >= 0; i --) {
                if (i == m - 1 && j == n - 1)   continue;
                if (i + 1 < m) {
                    dp[i][j] = min(dungeon[i][j], dp[i + 1][j] + dungeon[i][j]); 
                } else dp[i][j] = -9999;
                if (j + 1 < n) {
                    int temp = min(dungeon[i][j], dp[i][j + 1] + dungeon[i][j]);
                    dp[i][j] = max(dp[i][j], temp);
                }

                // cout << "[" << i << "," << j << "]" << " " << dp[i][j] << endl;
            }
        }
        if (dp[0][0] < 0) res = -dp[0][0] + 1;
        
        return res;
    }
};

那么还有一种行不通的方法,固定骑士的位置。dp[i][j]表示公主在(i,j)位置上时,骑士救到公主所耗的最低健康指数。base case就是公主就在骑士的房间里,dp[0][0] = dungeon[0][0]

为什么行不通呢?动态规划是从base case开始,自底向上递推。那么在递推的过程中,我们还需要一个数组memo[i][j],来记录骑士从(0, 0)来到(i,j)位置上的健康情况。只有知道了这个,我们才能完成自底向上的递推。这时我们发现dp[i][j]不仅与dp[i - 1][j], dp[i][j - 1]有关,还与memo[i - 1][j], memo[i][j - 1]有关。

先附上错误代码:

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        

        int m = dungeon.size();
        int n = dungeon[0].size();

        int dp[m][n];       //dp[i][j]记录当骑士救到i、j位置上的公主时在最好情况下最危险的健康点数
        int memo[m][n];     //memo[i][j]记录当骑士在最好情况下就到i、j位置上的公主时,此时的健康点数
        dp[0][0] = dungeon[0][0];
        memo[0][0] = dungeon[0][0];
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (i == 0 && j == 0)   continue;
                if (i - 1 >= 0) {
                    memo[i][j] = memo[i - 1][j] + dungeon[i][j];
                    if (memo[i][j] < dp[i - 1][j]) {
                        dp[i][j] = memo[i][j];
                    } else {
                        dp[i][j] = dp[i - 1][j];
                    }
                } else dp[i][j] = -99999;

                if (j - 1 >= 0) {
                    int res = memo[i][j - 1] + dungeon[i][j];
                    if (res < dp[i][j - 1]) {       
                        if (dp[i][j] < res) {
                            dp[i][j] = res;
                            memo[i][j] = res;
                        }
                    } else {
                        if (dp[i][j] < dp[i][j - 1]) {
                            if (dp[i][j] < dp[i][j - 1]) {
                                dp[i][j] = dp[i][j - 1];
                                memo[i][j] = res;
                            }
                        }
                    }
                }
            }
        }
        if (dp[m - 1][n - 1] > 0)   return 1;
        return -dp[m - 1][n - 1] + 1;
    }
};

36 / 45 个通过测试用例,在[[1,-3,3],[0,-2,0],[-3,-3,-3]]样例中发生错误。

错误的原因我对每一个i、j,都优先求最小的dp[i][j], memo[i][j]跟随dp[i][j]最小的情况。可是由dp[i][j]递推到dp[i + 1][j]的时候,dp[i + 1][j]不仅与dp[i][j]有关,还与memo[i][j]有关。

也就是说,如果设x为公主在i、j时,骑士救到公主,在他走过的所有路径中,每一条路径耗费的最高健康指数。y为骑士通过所有可能的路径来到i、j时,其当前体力。存在(x, y),x比dp[i][j]大,y比memo[i][j]小,并且在由i、j递推到dp[i + 1][j]时,所得的结果优于(dp[i][j], memo[i][j])。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值