LeetCode 174. 地下城游戏
题目描述


题意分析
从左上角出发,每次只能向右或向下,最终到达右下角。进入每个格子立即按其数值增减血量,起点和终点也要结算;任何时刻血量降到 0 或以下都会失败。要求进入起点之前最少需要多少血量。
解法:逆向动态规划
核心思路
[!blue]
倒推每个位置进入前的最低血量,而不是只计算整条路径的总收益。 总收益足够不代表沿途没有提前死亡。定义dp[i][j]为尚未结算当前格子时,为了从这里安全走到终点所需的最少血量,它已经包含未来每一步的存活要求。从当前格子可以去右边或下面,选后继需求更小的一条路,令
minNext = min(dp[i][j+1], dp[i+1][j])。若进入当前格子前有h点血,结算后就有h+dungeon[i][j],它必须不少于minNext,同时进入前也必须满足h>=1。两个条件合起来就是
dp[i][j] = max(1, minNext-dungeon[i][j])。这个血量既能活着进入当前格子,也能在结算后达到所选后继的最低要求;再少则会违反其中一个条件,所以它恰好是最小值。扣血格提高进入需求,补血格降低进入需求,但不能降到 0。终点结算后只需剩下至少 1 点血,因此其需求是
max(1,1-dungeon[m-1][n-1])。代码多开一行一列,把终点下方和右方的虚拟后继设为 1,使终点也能使用同一转移;其他越界位置设为极大值,表示不能从那里离开地牢。
解题步骤
- 创建
(m+1)×(n+1)的状态表并初始化为极大值,只令dp[m][n-1]与dp[m-1][n]为 1。- 行从下往上、列从右往左枚举,保证右侧和下侧状态已经计算完成。
- 取两个后继需求的较小值,减去本格数值,再与 1 取最大,写入当前状态。
- 返回
dp[0][0],即进入起点前的最低血量。单格地牢直接使用终点公式。只有一行或一列时,非法方向的极大值会让转移自动选择唯一合法方向;即使所有格子都能补血,初始血量也至少为 1。
代码实现
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int m = dungeon.length;
int n = dungeon[0].length;
// 多开一行一列哨兵,inf 表示该方向走不通。
int[][] dp = new int[m + 1][n + 1];
int inf = 1 << 30;
for (int i = 0; i <= m; i++) {
Arrays.fill(dp[i], inf);
}
// 终点的下方与右方置 1:通关后留 1 点血即可。
dp[m][n - 1] = 1;
dp[m - 1][n] = 1;
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
int minNext = Math.min(dp[i + 1][j], dp[i][j + 1]);
// max(1, ...) 保证任何时刻血量不低于 1。
dp[i][j] = Math.max(1, minNext - dungeon[i][j]);
}
}
return dp[0][0];
}
}
func calculateMinimumHP(dungeon [][]int) int {
m, n := len(dungeon), len(dungeon[0])
// 多开一行一列哨兵,inf 表示该方向走不通。
const inf = 1 << 30
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
for j := range dp[i] {
dp[i][j] = inf
}
}
// 终点的下方与右方置 1:通关后留 1 点血即可。
dp[m][n-1] = 1
dp[m-1][n] = 1
for i := m - 1; i >= 0; i-- {
for j := n - 1; j >= 0; j-- {
minNext := min(dp[i+1][j], dp[i][j+1])
// max(1, ...) 保证任何时刻血量不低于 1。
dp[i][j] = max(1, minNext-dungeon[i][j])
}
}
return dp[0][0]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
复杂度分析
- 时间复杂度:$O(mn)$,每格计算一次。
- 空间复杂度:$O(mn)$,保存带边界的状态表。
关键点总结
[!green]
- 状态定义在本格结算之前。
- 负格提高需求,正格降低需求,但不能低于一。
- 可以自主选择路线,所以后继需求取较小值。
易错点总结
[!yellow]
- 下限写成零:零血量已经死亡。
- 将本格数值加到需求上:补血与扣血的影响被反转。
- 所有边界都设为一:允许未到终点就从边缘退出。
- 返回终点状态:答案应是进入起点前的需求。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 64. 最小路径和 | 中等 | 同样在网格路径上DP,原题最小化累计代价,本题还要求每个前缀生命值为正,需从终点反推最低需求。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!