力扣原题链接,点击跳转。
有一个地下城,我们用m×n大小的二维数组dungeon表示。dungeon[i][j]如果为正,该位置有血包,可以加血量;如果为负,该位置有恶魔,会扣掉相应的血量。有一个骑士在左上角,有一个公主在右下角。骑士为了尽快解救公主,决定每次只往下走一步,或者往右走一步。他的血量降到0或者负数,就会立刻死亡。任何房间都可能对骑士的血量造成威胁,也可能增加骑士的血量,包括骑士进入的左上角房间以及公主被监禁的右下角房间。请问骑士的初始血量至少是多少,才能解救公主呢?
我们用动态规划的思想来解决这个问题。创建dp表,首先确定状态表示:究竟应该用dp[i][j]表示从左上角到达(i,j)的初始血量至少是多少,还是表示从(i,j)到达右下角的初始血量至少是多少呢?如果是前者,是推不出状态转移方程的,这是因为右下方的恶魔有可能对血量造成威胁,但是(i,j)最近的一步是(i-1,j)和(i,j-1),无法把右下方的影响考虑在内。故而,我们采取后一种状态表示。
在到达(i,j)后,下一步可能前往(i+1,j)或(i,j+1)。如果前往(i+1,j),应满足dp[i][j]+dungeon[i][j]=dp[i+1][j];同理,如果前往(i,j+1),应满足dp[i][j]+dungeon[i][j]=dp[i][j+1]。然而,我们要让dp[i][j]尽可能小,所以选择能让dp[i][j]更小的路径,即dp[i][j]=min(dp[i+1][j],dp[i][j+1])-dungeon[i][j]。这里还需要考虑一个问题,如果dungeon[i][j]是一个很大的血包,使得dp[i][j]≤0,那么就要把dp[i][j]设置成1,因为血量必须为正数。故状态转移方程为:dp[i][j]=max(1,min(dp[i+1][j],dp[i][j+1])-dungeon[i][j])。
接着考虑初始化的问题。由于dp[i][j]只和右边和下边的值相关,所以应初始化最下边一行和最右边一列,防止越界。
??
? ? ? ? ?
我们用一种更加优雅的方式来初始化,在最下边和最右边添加一行一列。
? *? *
? ? ? ? ? *
* * * * * *
考虑右下角那个?的值,我们可以理解为:到达这个?后,向下走和向右走,都至少需要1点血量,那么这个?下边和右边的位置应初始化为1。当然,你也可以带入状态转移方程,只有这么初始化才是合理的。
? *? *
? ? ? ? ? 1
* * * * 1 *
接着考虑其他?位置。为了在求min(dp[i+1][j],dp[i][j+1])时,*位置的值不影响结果,最好把*位置的值都初始化为+∞。由于dp表每个位置的值只和右边和下边的值相关,故应从下往上填每一行,每一行从右往左填表。最后,应返回dp[0][0]的值。
class Solution
{
public:int calculateMinimumHP(vector<vector<int>>& dungeon){// 创建dp表int m = dungeon.size(), n = dungeon[0].size();vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));// 初始化dp[m - 1][n] = dp[m][n - 1] = 1;// 填表for (int i = m - 1; i >= 0; i--)for (int j = n - 1; j >= 0; j--)dp[i][j] = max(1, min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]);return dp[0][0];}
};