问题描述
174. 地下城游戏https://leetcode-cn.com/problems/dungeon-game/
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径
右 -> 右 -> 下 -> 下
,则骑士的初始健康点数至少为 7。-2 (K) -3 3 -5 -10 1 10 30 -5 (P)
说明:
骑士的健康点数没有上限。
任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
问题分析
-
骑士的生命值HP最小值为1,不能为0。
-
通过动态规划DP算法求解。注意,逆向DP可以容易解决问题,而正向DP极其复杂且容易陷入误区。以下通过示例进行说明。
-
看到题目后,我们惯性的会先考虑到正向DP,也就是:在m*n的区域R中,要求解$R_{min}[m-1, n-1]$,由于骑士K只能向下或向右移动一步,那么递推公式为$R_{min}[i, j] = min(R_{min}[i-1, j]+f(i, j), R_{min}[i, j-1]+f(i, j))$,也就是根据K到点(i,j)的左侧和上侧的最小HP,结合(i,j)的影响确定新的HP。其中,对于$f(i,j)$,有: \(f(i,j)=\left\{\begin{array}{l} -(i, j), (i, j) \le 0 \\ 0, (i,j)>0 \end{array}\right.\) 上式容易理解,如果点(i,j)的值小于等于0,那么K进入该至少需要再增加$-(i,j)$的HP;如果点(i,j)的值大于零0,无需增加HP,K只要能到达上一点,则必然可到达点(i,j),但这时问题出现了:在先前的考虑中,为取得最小HP,我们默认K到达某一点时的HP最小,即为1,而点(i,j)补充的生命值就需要另外一个数组存储下来,在后续的行进中优先消耗新增的HP,直到为0后才考虑增加K的初始HP。为此,正向DP不仅需要一个二维数组保存K到达某点的最小初始HP,还需要一个二维数组保存当前的HP,增加空间复杂度,同时更多的判断操作也增加了时间复杂度。至此可以发现,正向DP可以求解,但程序复杂,那么考虑逆向DP呢?转化一下思路,不考虑K到达每一个点所需的最小HP,而是考虑——要到达公主P点,从每个点出发所需的最小HP大小。
-
对于逆向DP:在m*n的区域R中,要求解$R_{min}[0, 0]$,由于前一节点只能来自右侧或下侧,递推公式$R_{min}[i, j] = min(R_{min}[i+1, j]+f(i, j), R_{min}[i, j+1]+f(i, j))$,即根据前一步的最小HP,结合(i,j)的影响确定新的HP。其中,对于$f(i,j)$,有: \(f(i,j)=\left\{\begin{array}{l} -(i, j), (i, j) \le 0 \\ 1-R_{min}[before], (i,j) \ge R_{min}[before] \\ -(i,j), else \end{array}\right.\) 也就是说,如果点(i,j)的值小于等于0,那么相对前一节点,从(i,j)K进入该至少需要再增加$-(i,j)$的HP,哪怕前一节点是极大的奖励HP,因为在K的运行中,只有保证先到达(i,j),才能继续向右或向下行进,这就是”将来不会影响过去,但过去会影响将来“,这就避免了正向DP中对未来路径奖励HP的不确定性;如果点(i,j)的值大于0,那么与前一节点做判断,大于前一节点所需的最小HP则K满足最小值1即可,否则补充不足于前一节点的HP。简化得到递推公式: \(R_{min}[i, j] = min(max(R_{min}[i+1, j]-(i, j), 1), max(R_{min}[i, j+1]-(i, j),1))\)
-
此外,边界情况(即逆向DP最上端和最左端只有一个转移方向)需要单独定义。
-
实现代码
class Solution(object):
def calculateMinimumHP(self, dungeon):
"""
逆向DP
:type dungeon: List[List[int]]
:rtype: int
"""
M = len(dungeon)
N = len(dungeon[0])
all_HP = [[0]*N]*M
for i in range(M-1, -1, -1):
for j in range(N-1, -1, -1):
if i==M-1 and j==N-1:
all_HP[i][j] = max(-dungeon[i][j], 0) + 1
elif i==M-1 and j!=N-1:
all_HP[i][j] = max(all_HP[i][j+1]-dungeon[i][j], 1)
elif j==N-1 and i!=M-1:
all_HP[i][j] = max(all_HP[i+1][j]-dungeon[i][j], 1)
else:
all_HP[i][j] = min(max(all_HP[i][j+1]-dungeon[i][j], 1),
max(all_HP[i+1][j]-dungeon[i][j], 1))
return all_HP[0][0]