174. Dungeon Game [Hard]

【动态规划思路】

典型需要逆向动态规划的题目:

用dp数组的每个位置保存进入地牢中每个对应位置的时候,为了保证到终点不死,骑士需要的最少血量

dp数组在最初就能确定的值只有右下角,只要保证骑士进入血量减去输入数组对应位置的血量能剩余1,据此计算出dp数组右下角的值

dp数组其他的值都依赖右下角来推算,所以动态规划应该从右下角到左上角

【递归思路】

可以用递归 + 数组保存已计算结果的方式,用左上到右下的顺序解答本题

代码很简洁,不需要对dp数组做麻烦的初始化操作,而且是所有方法里最快的,学到了,详见最后一段代码

【实现】

dp[i][j](即为了保证到终点不死,进入每个位置的最低血量)的计算:

进入最小血量有两个条件:进入时候必须活着,出去时满足进入下一个位置的最小血量

进入最小血量 = Math.max(1, 离开最小血量 - 地牢中对应位置的数值)

—— 前者为了活着,后者为了进入下一个位置

计算步骤:

1. 计算离开时的最小血量min:

计算依赖于dp[i][j+1]和dp[i+1][j]的值,两者间较小的为离开该位置时候的最小血量,min = Math.min(dp[i][j+1], dp[i+1][j])

2. 计算进入时的最小血量dp[i][j]:

若原始数组中该位置的值➕1大于或等于该最小血量的值(➕1是因为进入任何一个都有1的初始血量),即dungeon[i][j] + 1 >= min,则进入该位置时只需要活着,dp[i][j] = 1

若是➕1小于该最小血量的值,则进入该位置时要有更多的血以满足走后面的格子的需求,dp[i][j] = min - dungeon[i][j]

/**
 * 自己的代码
 * 从终点到起点,右下到左上做动态规划
 * dp数组每一个值代表从当前位置走到终点骑士不死的最低进入血量(注意不死的最低血量是1不是0)
 * Runtime: 1 ms, Memory Usage: 38.6 MB
 */
class Solution {
    public int calculateMinimumHP(int[][] dungeon) {
        int m = dungeon.length, n = dungeon[0].length;
        int[][] dp = new int[m][n];
        dp[m - 1][n - 1] = dungeon[m - 1][n - 1] >= 0 ? 1 : 1 - dungeon[m - 1][n - 1]; // 初始化dp数组右下角
        for (int i = n - 2; i >= 0; i--) // 初始化dp数组最后一行
            dp[m - 1][i] = dungeon[m - 1][i] + 1 >= dp[m - 1][i + 1] ? 1 : dp[m - 1][i + 1] - dungeon[m - 1][i];
        for (int i = m - 2; i >= 0; i--) {
            dp[i][n - 1] = dungeon[i][n - 1] + 1 >= dp[i + 1][n - 1] ? 1 : dp[i + 1][n - 1] - dungeon[i][n - 1]; // 初始化dp数组最后一列
            for (int j = n - 2; j >= 0; j--) {
                int min = Math.min(dp[i + 1][j], dp[i][j + 1]);
                dp[i][j] = dungeon[i][j] + 1 >= min ? 1 : min - dungeon[i][j];
            }
        }
        return dp[0][0]; // 返回dp数组左上角
    }
}
/**
 * 直接用递归的方法,代码没问题但会Time Limit Exceeded
 * 改进的方法是用dp数组存储计算过的位置
 */
class Solution {
    int[][] dungeon;
    int row, col;
    public int calculateMinimumHP(int[][] dungeon) {
        this.dungeon = dungeon;
        row = dungeon.length;
        col = dungeon[0].length;
        return helper(0, 0);
    }
    private int helper(int i, int j) {
        if (i == row - 1 && j == col - 1)
            return Math.max(1, 1 - dungeon[row - 1][col - 1]);
        if (i >= row || j >= col)
            return Integer.MAX_VALUE;
        int min = Math.min(helper(i + 1, j), helper(i, j + 1));
        return Math.max(1, min - dungeon[i][j]);
    }
}
/**
 * 递归 + 数组保存已计算结果
 * 这种方法的速度是最快的
 * Runtime: 0 ms, faster than 100.00%
 * Memory Usage: 38.7 MB, less than 45.67%
 */
class Solution {
    int[][] dungeon, dp;
    int row, col;
    public int calculateMinimumHP(int[][] dungeon) {
        this.dungeon = dungeon;
        row = dungeon.length;
        col = dungeon[0].length;
        dp = new int[row][col];
        return helper(0, 0);
    }
    private int helper(int i, int j) {
        if (i == row - 1 && j == col - 1) // 右下角的最低进入血量是确定的
            return Math.max(1, 1 - dungeon[row - 1][col - 1]); 
        if (i >= row || j >= col) // 最后一行和最后一列超出边界的方向,将最低进入血量设为Integer.MAX_VALUE
            return Integer.MAX_VALUE;
        if (dp[i][j] > 0) // 已经计算过的位置,直接从数组中取值
            return dp[i][j];
        int min = Math.min(helper(i + 1, j), helper(i, j + 1)); // 找出向右、向下两种方式中要求血量的较低值
        dp[i][j] = Math.max(1, min - dungeon[i][j]); // 每计算出来一个新位置的值,存入dp数组
        return dp[i][j];
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值