地下城游戏 leetcode 174

题目

恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。

返回确保骑士能够拯救到公主所需的最低初始健康点数。

注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

示例 1:
输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7
解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。

示例 2:
输入:dungeon = [[0]]
输出:1
提示:
m == dungeon.length
n == dungeon[i].length
1 <= m, n <= 200
-1000 <= dungeon[i][j] <= 1000

思路

  1. 路径问题,利用动态规划解题
  2. 新建dp表,确定填表顺序、初始化以及返回值

要点

一. 状态表达式(参照力扣官方题解)

1. dp[i][j]表示到达(i,j)位置所需的健康值时为什么不行?

从 (0,0)到 (1,2)有多条路径,取其中最有代表性的两条:地下城游戏 leetcode 174_第1张图片
地下城游戏 leetcode 174_第2张图片
在上图中,我们知道应该选取绿色路径,因为蓝色路径的路径和太小,使得蓝色路径需要增大初始值到 4 才能走到终点,而绿色路径只要 3 点初始值就可以直接走到终点。但是如果把终点的 −2 换为 0,蓝色路径只需要初始值 2,绿色路径仍然需要初始值 3,最优决策就变成蓝色路径了。

因此,如果按照从左上往右下的顺序进行动态规划,我们无法直接确定到达 (1,2)的方案,因为前方的路径选择(剩余健康点)后方的路径选择(初始化健康点)两个参数同时影响后续的决策。也就是说,这样的动态规划是不满足「无后效性」的。

于是考虑从右下往左上进行动态规划。令 dp[i][j] 表示从坐标 (i,j)到终点所需的最小初始值。

2. 确定状态表达式

  1. 以最近的一步确定状态表达式,从坐标 (i,j)到终点,最近的一步:要么往下走,要么往右走。
  2. 往右走:(i,j)位置的最低健康点数(初始化值)加上(i,j)位置的dungeon[i][j]对健康值的影响需要大于等于(i,j+1)位置的最低健康点数,这样才能顺利通过下一个位置到达终点。则有dp[i][j] + dungeon[i][j] >= dp[i][j+1],即dp[i][j] >= dp[i][j+1] - dungeon[i][j]
  3. 往下走:同理,(i,j)位置的最低健康点数(初始化值)加上(i,j)位置的dungeon[i][j]对健康值的影响需要大于等于(i+1,j)位置的最低健康点数,这样才能顺利通过下一个位置到达终点。则有dp[i][j] + dungeon[i][j] >= dp[i+1][j],即dp[i][j] >= dp[i+1][j] - dungeon[i][j]

二、状态转移方程

  1. 综上两种情况,(i,j)位置的最低健康点数dp[i][j]应该等于两种情况的最小值,dp[i][j] = Math.min(dp[i][j+1],dp[i+1][j]) - dungeon[i][j]
  2. 观察上面状态转移方程,当dungeon[i][j]的值很大时,最小初始化值dp[i][j]可能为负数。也就是说,(i,j)位置有一个超级大的魔法球(血包),即使最小初始化值dp[i][j]为负数,吃了血包之后依旧可以通过下一个位置到达终点。但是这是不符合实际要求的,那么最小初始化值dp[i][j]需要对1求max,保证最小初始化值dp[i][j]至少为1。
  3. dp[i][j] = Math.max(Math.min(dp[i][j+1],dp[i+1][j]) - dungeon[i][j],1)

三、初始化

  1. 根据上述状态转移方程,dp[i][j]的值受右方dp[i][j+1]和下方dp[i+1][j]的值影响,为了保证数组不越界,需要在dp表的最右方和最下方新增一列、一行。
  2. 和题目相同示例(索引从0开始),终点为(2,2),dungeon[2][2]值为-2。要保证救出公主后,仍有健康点(最少仍有一点健康值),dp[2][3]和dp[3][2]的值应该为1。
  3. 新增的一行一列位置不能被骑士作为路径选择,路径选择的依据是Math.min(dp[i][j+1],dp[i+1][j]),也即新增位置的值对min()函数取结果时不能被算上。那么新增的位置就可设置为无穷大,不会对正常路径造成影响。
  4. 因为是在最右方和最下方新增虚拟位置,dp表索引和dungeon[i][j]的索引相同
    地下城游戏 leetcode 174_第3张图片

四、填表顺序

根据状态转移方程可知,dp[i][j]的值依赖于下方dp[i+1][j]右方dp[i][j+1]的值,填表时需要先求出这部分的值,那么填表的顺序应该为从下往上,从右往左

五、返回值

dp[i][j] 表示从坐标 (i,j)到终点所需的最小初始值,那么返回值应该为dp[0][0]

代码

    public int calculateMinimumHP(int[][] dungeon) {
        int m = dungeon.length,n = dungeon[0].length;
        int[][] dp = new int[m + 1][n + 1];  //1.新建dp表

        for(int i = 0;i <= m;i ++) dp[i][n] = Integer.MAX_VALUE;  //2.初始化   初始化最右一列
        for(int i = 0;i <= n;i ++) dp[m][i] = Integer.MAX_VALUE;  //          初始化最下一行
        dp[m - 1][n] = dp[m][n - 1] = 1;                          //          初始化特殊两个位置

        for(int i = m - 1;i >= 0;i --){  //3. 填表顺序   从下往上
            for(int j = n - 1;j >= 0;j --){  //         从右往左
                dp[i][j] = Math.max(Math.min(dp[i][j+1],dp[i+1][j]) - dungeon[i][j],1);
            }
        }

        return dp[0][0];  //4.确定返回值
    }

你可能感兴趣的:(刷题,leetcode,动态规划,算法)