174. 地下城游戏
力扣题解:添加链接描述
难度困难555
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下
,则骑士的初始健康点数至少为 7。
-2 (K) | -3 | 3 |
---|---|---|
-5 | -10 | 1 |
10 | 30 | -5 § |
说明:
- 骑士的健康点数没有上限。
- 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
解析:
- 合理设计
dp
数组/函数的定义。类比前文 最小路径和问题,dp
函数签名肯定长这样:
int dp(int[][] grid, int i, int j);
-
dp
函数的定义应该是:从
grid[i][j]
到达终点(右下角)所需的最少生命值是dp(grid, i, j)
。按照最小路径和dp数组的定义,只知道「能够从左上角到达
B
的最小生命值」,但并不知道「到达B
时的生命值」。但是「到达
B
时的生命值」是进行状态转移的必要参考,我给你举个例子你就明白了,假设下图这种情况:你说这种情况下,骑士救公主的最优路线是什么?
显然是按照图中蓝色的线走到
B
,最后走到A
对吧,这样初始血量只需要 1 就可以;如果走黄色箭头这条路,先走到C
然后走到A
,初始血量至少需要 6。为什么会这样呢?骑士走到
B
和C
的最少初始血量都是 1,为什么最后是从B
走到A
,而不是从C
走到A
呢?因为骑士走到
B
的时候生命值为 11,而走到C
的时候生命值依然是 1。如果骑士执意要通过
C
走到A
,那么初始血量必须加到 6 点才行;而如果通过B
走到A
,初始血量为 1 就够了,因为路上吃到血瓶了,生命值足够抗A
上面怪物的伤害。这下应该说的很清楚了,再回顾我们对
dp
函数的定义,上图的情况,算法只知道dp(1, 2) = dp(2, 1) = 1
,都是一样的,怎么做出正确的决策,计算出dp(2, 2)
呢?,所以要修改dp的定义 -
base case 就是
i=m - 1, j
= n - 1的时候,保证骑士不能死需要的生命值。if (i == m - 1 && j == n - 1) { return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; }
-
状态转移:根据新的
dp
函数定义和 base case,我们想求dp(0, 0)
,那就应该试图通过dp(i, j+1)
和dp(i+1, j)
推导出dp(i, j)
,这样才能不断逼近 base case,正确进行状态转移。「从
A
到达右下角的最少生命值」应该由「从B
到达右下角的最少生命值」和「从C
到达右下角的最少生命值」推导出来:能不能推导出来呢?这次是可以的,假设
dp(0, 1) = 5, dp(1, 0) = 4
,那么可以肯定要从A
走向C
,因为 4 小于 5 嘛。那么怎么推出
dp(0, 0)
是多少呢?假设
A
的值为 1,既然知道下一步要往C
走,且dp(1, 0) = 4
意味着走到grid[1][0]
的时候至少要有 4 点生命值,那么就可以确定骑士出现在A
点时需要 4 - 1 = 3 点初始生命值,对吧。那如果
A
的值为 10,落地就能捡到一个大血瓶,超出了后续需求,4 - 10 = -6 意味着骑士的初始生命值为负数,这显然不可以,骑士的生命值小于 1 就挂了,所以这种情况下骑士的初始生命值应该是 1。综上,状态转移方程已经推出来了:
int res = min( dp(i + 1, j), dp(i, j + 1) ) - grid[i][j]; dp(i, j) = res <= 0 ? 1 : res;
- 备忘录消除重叠子问题
代码:
class Solution {
int[][] memo;
public int calculateMinimumHP(int[][] dungeon) {
int m=dungeon.length;
int n=dungeon[0].length;
memo=new int[m+1][n+1];
for(int[] row: memo){
Arrays.fill(row,-1);
}
return dp(dungeon,0,0);//从左上角到右下角需要的最少的生命值
}
public int dp(int[][] dungeon,int i,int j){
int m=dungeon.length;
int n=dungeon[0].length;
//basecase
if(i==m-1 && j== n-1) return dungeon[i][j]>=0? 1:-dungeon[i][j]+1;
if(i==m || j==n) return Integer.MAX_VALUE;
if(memo[i][j]!=-1) return memo[i][j];
int res=Math.min(dp(dungeon,i+1,j),dp(dungeon,i,j+1))-dungeon[i][j];
memo[i][j]=res<=0? 1:res;
return memo[i][j];
}
}