题目链接
LeetCode 174. 地下城游戏[1]
题目描述
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
-2(K) | -3 | -3 |
-5 | -10 | 1 |
10 | 30 | -5(P) |
提示:
- 骑士的健康点数没有上限。
- 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
题解
错误解法
首先我们肯定想到的是从左上到右下动态规划,那么对于 这个格子来说,它有两个选择,可以从 或者 过来。
我们令 表示从左上角走到 这个格子所需要的最小生命值,那么我们选择 ,也就是两个来向中较小的那个走过来。但是考虑了当前格子的数值之后,路线上所需生命的最小值是可能增大的,而这时候可能选择两个来向中较大的那个反而更好(因为那个来向数值之和比较大),所以这里就产生了矛盾,无法求解。
举个简单的例子:
1(K) | -3 | 3 |
0 | -2 | 0 |
-3 | -3 | -3(P) |
这个例子中如果只看走到格子 的结果的话,肯定是 下 -> 右 -> 右 最好,因为这样初始生命只需要 2 就够了。而另一条路 右 -> 右 -> 下 则需要初始生命 3 。
但是如果继续走到格子 ,那么最优方向一定是从 过来(另一个方向负数太多)。但是到 的最优路线保存的是 下 -> 右 -> 右 这一条,走到终点总和是 -4 ,初始所需最小生命增大为 5 。而另一条原本不怎么好的路线 右 -> 右 -> 下 总和是 -2 ,初始所需最小生命 3 ,所以仍然保持不变。
这样看来原本不好的路线在最后的结果里是可能会变好的,所以不好保存下来直接递推。
正确解法
既然从左上到右下没法动态规划,我们不妨从右下到左上动态规划看看。
我们令 表示从 这个格子走到右下角所需要的最小生命值,同样我们选择两个去向中的较小值 。然后考虑了格子 之后, 就更新为:
为什么这里选择两个去向中所需初始生命较小的那个就没问题了呢?
严格证明
考虑上图这种情况,这里我把 抽象为了 ,右边一格抽象为了 ,右下角抽象为了 。然后 走下面这条路所需初始生命值最小,路径上格子记为 ,另一条路径上格子记为 。
因为走路径 所需的初始生命值更小,所以我们有:
等价于:
这时候我们在两边 里面同时加上 ,大小关系是不会变的。
而错误解法中,考虑下图这种情况:
同样我们可以得到:
到这里为止和上面正确解法是一模一样的。但是,加上 之后,和上面正解的区别就是,正解求和里每一项都加了,所以大小关系不变,但是错解只有一项加了(就是所有值全加起来),大小关系无法确定。
代码
c++
class Solution { public: int calculateMinimumHP(vector<vector<int>>& dungeon) { int n = dungeon.size(), m = dungeon[0].size(); vector<vector<int>> dp(n+1, vector<int>(m+1, INT_MAX)); dp[n][m-1] = dp[n-1][m] = 1; for (int i = n-1; i >= 0; --i) { for (int j = m-1; j >= 0; --j) { int minn = min(dp[i+1][j], dp[i][j+1]); dp[i][j] = max(1, minn-dungeon[i][j]); } } return dp[0][0]; } };