今天遇到一道DP题,记录一下思路,以免遗忘。
题目:地下城游戏
题目来源: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) |
说明:
骑士的健康点数没有上限。
任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
题目解释:求一个初始值,使其从k点出发,保证到p点后,该初始值为1.
看到这种题目想都不用想,DFS回溯一定超时..套路太深,所以能想到的第二种方法就是动态规划。
做动态规划,首先要保证三点:1.找到最优子解
2.列出转换方程
3.找到终止条件
然后,就轮到这道题的磨人之处了,一般的DP题目,无论从左上角递推到右下角,还是从右下角到左上角,都是一样的,不过这道题除了一个最优路径,还有一个骑士生命值,受到这两个变量的影响,两个出发点的答案是不一定相同的(Fuck),最后才发现从左上角到右下角不一定是最优解,而从右下角到左上角一定是最优解,至于为什么,因为自上而下,一般来说都选扣血少的点走嘛,但是你无法保证你骑士的最初的生命值稳定,但是从下往上走,我们可以确定最终状态的骑士的最低血量是多少,然后根据情况叠加。
设矩阵行长度为n,列长度为m,左上角为0,0,右下角为n-1,m-1 ,dp[i][j]为矩阵i,j上的最优解,既然已经知道出发点,所以我们需要使dp[n+1][m] = dp[n][m+1] = 1;(所以在建立dp数组的时候,需要将长宽定义为n+1,m+1,且必须将边围值设为无限大),这样才能定义dp[n-1][m-1]的初值,因为我们要求的是初始生命值,且假如扣的生命值小于0时,骑士的血由于不可能是负数,所以得使他保持一个1以上的生命值,因为每次都要选择扣血最少的方向,那么我们可以得到转换方程dp[i][j] = max(1,min(dp[i+1][j],dp[i][j+1]) - dungeon[i][j]);(dungeon为题目矩阵),所以我们就可以递推到dp[0][0],也就是我们所需要的初始生命值。
代码:
class Solution { public: int calculateMinimumHP(vector<vector<int>>& dungeon) { int n = dungeon.size(); int m = dungeon[0].size(); if(!n&&!m) return {}; vector<vector<int>> dp(n+1, vector<int>(m+1, INT_MAX)); dp[n][m-1] = 1; dp[n-1][m] = 1; for(int i = n - 1 ; i >= 0 ; i--) for(int j = m - 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]; } };
总结:熟悉动归结构,确定三个条件。