持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情
题目(Dungeon Game)
链接:https://leetcode-cn.com/problems/dungeon-game
解决数:1012
通过率:48.4%
标签:数组 动态规划 矩阵
相关公司:uber google microsoft
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
| -2 (K) | -3 | 3 |
|---|---|---|
| -5 | -10 | 1 |
| 10 | 30 | -5 (P) |
说明:
- 骑士的健康点数没有上限。
- 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
思路
- 题目求骑士出发时所需要的最小稳妥血量,他走过一个点就会加上该点的权重(有正有负)。
- 每次有两种选择:向右走,或向下走
- 可以用递归做:
- “嗨,dfs函数,帮我算出:如果我走右边的点,我至少要带多少安全血量”
- “嗨,dfs函数,帮我算出:如果我走下边的点,我至少要带多少安全血量”
- “我根据上面的返回值,算出:走到我当前的点,我至少要带的安全血量”
未优化的代码(超时)
虽然超时了,但加入记忆化就能过了,很好改。代码我详细注释了。
const calculateMinimumHP = (dungeon) => {
const m = dungeon.length;
const n = dungeon[0].length;
const minSaveHP = (dungeon, i, j) => { // 返回:来到点(i,j)至少要带多少血量
if (i == m - 1 && j == n - 1) {
// 当来到公主坐标,如果它的权重为正,以1的血量来到这里即可
// 如果它的权重为负,则要带着 1-dungeon[i][j] 的血量来这
return dungeon[i][j] > 0 ? 1 : 1 - dungeon[i][j];
}
let goDown = Infinity, goRight = Infinity;
// 走下方的点,需要带着的最小安全血量
if (i < m - 1) goDown = minSaveHP(dungeon, i + 1, j);
// 走右方的点,需要带着的最小安全血量
if (j < n - 1) goRight = minSaveHP(dungeon, i, j + 1);
// 如果走下方,所需要带的血量更少
if (goDown < goRight) {
if (goDown - dungeon[i][j] <= 0) {
// goDown血量,是来到(i+1,j)的稳妥血量,等于,来到(i,j)的稳妥血量 + dungeon[i][j]
// 则,goDown血量-dungeon[i][j],就是来到(i,j)点的稳妥血量,如果 <= 0
// 则要它返回 1,即来到(i,j)点的稳妥血量至少要为 1
return 1;
} else {
// goDown血量,是来到(i+1,j)的稳妥血量,等于,来到(i,j)的稳妥血量 + dungeon[i][j]
// 则,goDown血量-dungeon[i][j],就是来到(i,j)点的稳妥血量,如果 > 0
// 则它是安全的,返回它本身即可,即 goDown血量 - dungeon[i][j]
return goDown - dungeon[i][j];
}
} else { // 如果走右方,需要带着的血量更少。分析类似上面
if (goRight - dungeon[i][j] <= 0) {
return 1;
} else {
return goRight - dungeon[i][j];
}
}
};
return minSaveHP(dungeon, 0, 0); // 至少需要带着多少血量来到(0,0)这点
};
加入记忆化,优化时间复杂度
用一个备忘录数组,存子调用的返回值,即,子问题的解。遇到相同重复的子问题,直接从memo里拿缓存值来用,降低时间复杂度
优化后
const calculateMinimumHP = (dungeon) => {
const m = dungeon.length;
const n = dungeon[0].length;
// memo初始化,每一项都为0,代表还没记录
const memo = new Array(m);
for (let i = 0; i < m; i++) {
memo[i] = new Array(n).fill(0);
}
const minSaveHP = (dungeon, i, j) => {
if (i == m - 1 && j == n - 1) { // 递归的出口
return dungeon[i][j] > 0 ? 1 : 1 - dungeon[i][j];
}
if (memo[i][j] > 0) return memo[i][j]; // 如果备忘录中有,就直接返回它
let goDown = Infinity, goRight = Infinity;
if (i < m - 1) // 走下方的点,需要带着的最小安全血量
goDown = minSaveHP(dungeon, i + 1, j);
if (j < n - 1) // 走右方的点,需要带着的最小安全血量
goRight = minSaveHP(dungeon, i, j + 1);
if (goDown < goRight) {
if (goDown - dungeon[i][j] <= 0) {
memo[i][j] = 1;
} else {
memo[i][j] = goDown - dungeon[i][j];
}
} else {
if (goRight - dungeon[i][j] <= 0) {
memo[i][j] = 1;
} else {
memo[i][j] = goRight - dungeon[i][j];
}
}
return memo[i][j];
};
return minSaveHP(dungeon, 0, 0, memo);
};