https://leetcode-cn.com/problems/dungeon-game/
题目给我们一个矩阵m * n,骑士在右上角,公主在左下角,让我们只能在向右或向下的条件下,寻找最好的、最安全的路径。
何为最安全呢?就是这条路径上,骑士的最低健康指数比其他路径都要高。
套路的基础是首先你要对问题讨价还价。第一步就是在”讨价还价“
第一步,在原问题的基础上缩小问题规模,并得到base case
什么叫做在原问题的基础上缩小规模呢?就是你缩小规模的那些问题必须是原问题的子问题。就好比让你求"abcbda"的最大回文子串,“abc”,"bcb"都是所谓的“在原问题基础上缩小问题规模“,但是”ecd“不是,它不被"abcbda"包含嘛。
这里有两种方法可以缩小问题规模,一个是固定骑士的位置,一个是固定公主的位置,并且有一种是行不通的。固定公主是可行的,我们先用这个方法。
拿样例举例。一个3 * 3的矩阵,骑士在左上角,公主在右下角。这个问题也太麻烦了,如果骑士不在左上角(0,0)处,而是在(1,1)处多好啊,这样就是解决一个2 * 2的迷宫,要比3 * 3好多了……骑士在(1,2),(0,2),(2,2)也可以啊,反正都比原问题简单,并且在(2,2)是最简单的情况,也就是base case。(这就是一个跟问题讨价还价的过程)
第二步,确定状态和选择
我们如何在原问题的基础上表达这些小规模的问题?矩阵问题通常是设两个两个指针i、j,(i,j)表示要求骑士在(i,j)位置时救到公主所耗的最少体力。这样每一个i、j都能表示一个子问题。
那么状态就是i、j的位置。
选择是什么?假设骑士在(i、j)位置上,他为了救公主会怎么做?向右或向下移动。
第三步,确定dp数组和状态转移方程
dp数组的参数很好确定,就是i、j。dp[i][j]的值是什么?往往就是题目要求的结果,在这里是骑士从这个位置开始,为了救公主,走最安全路径途中的最低健康指数。
状态转移方程呢?确定状态转移方程需要死卡dp数组的定义,并且假设比问题规模小的那些子问题已知。也就是说,如果我们要求dp[i][j],那么比状态i、j小的子问题都已知。
先附上状态转移的代码:
dp[i][j] = min(dungeon[i][j], dp[i + 1][j] + dungeon[i][j], dp[i][j + 1]) + dungeon[i][k]
什么意思呢?骑士在i、j位置可以向右走或者向下走。如果骑士选择向右走,那么骑士就来到了(i , j + 1)位置,并且我们已经知道了骑士初始在(i、j + 1)的位置上,走最安全路径途中的最低健康指数。看一下定义,我们从定义中可以知道,骑士如果只能向右走,那么所走的最安全的路径就是初始在(i、j + 1)的位置上,走的最安全路径。并且在这个路径上,骑士的健康指数都是在(i、j + 1)的基础上,加上dungeon[i][j]。
所以最低健康指数就是dp[i][[j + 1] + dungeon[i][j]吗?不一定。dungeon[i][j]也可能是最低的嘛。
向下同理,因此我们得到在不同选择下的dp[i][j]为
dp[i][j] = min(dungeon[i][j], dp[i + 1][j] + dungeon[i][j], dp[i][j + 1]) + dungeon[i][k]
第四步 确定遍历顺序
怎么遍历呢?这一步还是挺简单的。其实就是让你设计一个遍历方法,如果来到了状态i、j,要保证状态i + 1, j和状态i, j + 1都已知嘛。这是一个自底向下的过程,画一个二维表就可以解决。遍历顺序是
for (int j = n - 1; j >= 0; j --) for (int i = m - 1; i >= 0; i --)
在图上一画就明白了。
附上代码:
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int m = dungeon.size();
int n = dungeon[0].size();
int dp[m + 2][n + 2];
dp[m - 1][n - 1] = dungeon[m - 1][n - 1];
int res = 1;
for (int j = n - 1; j >= 0; j --) {
for (int i = m - 1; i >= 0; i --) {
if (i == m - 1 && j == n - 1) continue;
if (i + 1 < m) {
dp[i][j] = min(dungeon[i][j], dp[i + 1][j] + dungeon[i][j]);
} else dp[i][j] = -9999;
if (j + 1 < n) {
int temp = min(dungeon[i][j], dp[i][j + 1] + dungeon[i][j]);
dp[i][j] = max(dp[i][j], temp);
}
// cout << "[" << i << "," << j << "]" << " " << dp[i][j] << endl;
}
}
if (dp[0][0] < 0) res = -dp[0][0] + 1;
return res;
}
};
那么还有一种行不通的方法,固定骑士的位置。dp[i][j]表示公主在(i,j)位置上时,骑士救到公主所耗的最低健康指数。base case就是公主就在骑士的房间里,dp[0][0] = dungeon[0][0]
为什么行不通呢?动态规划是从base case开始,自底向上递推。那么在递推的过程中,我们还需要一个数组memo[i][j],来记录骑士从(0, 0)来到(i,j)位置上的健康情况。只有知道了这个,我们才能完成自底向上的递推。这时我们发现dp[i][j]不仅与dp[i - 1][j], dp[i][j - 1]有关,还与memo[i - 1][j], memo[i][j - 1]有关。
先附上错误代码:
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int m = dungeon.size();
int n = dungeon[0].size();
int dp[m][n]; //dp[i][j]记录当骑士救到i、j位置上的公主时在最好情况下最危险的健康点数
int memo[m][n]; //memo[i][j]记录当骑士在最好情况下就到i、j位置上的公主时,此时的健康点数
dp[0][0] = dungeon[0][0];
memo[0][0] = dungeon[0][0];
for (int i = 0; i < m; i ++) {
for (int j = 0; j < n; j ++) {
if (i == 0 && j == 0) continue;
if (i - 1 >= 0) {
memo[i][j] = memo[i - 1][j] + dungeon[i][j];
if (memo[i][j] < dp[i - 1][j]) {
dp[i][j] = memo[i][j];
} else {
dp[i][j] = dp[i - 1][j];
}
} else dp[i][j] = -99999;
if (j - 1 >= 0) {
int res = memo[i][j - 1] + dungeon[i][j];
if (res < dp[i][j - 1]) {
if (dp[i][j] < res) {
dp[i][j] = res;
memo[i][j] = res;
}
} else {
if (dp[i][j] < dp[i][j - 1]) {
if (dp[i][j] < dp[i][j - 1]) {
dp[i][j] = dp[i][j - 1];
memo[i][j] = res;
}
}
}
}
}
}
if (dp[m - 1][n - 1] > 0) return 1;
return -dp[m - 1][n - 1] + 1;
}
};
36 / 45 个通过测试用例,在[[1,-3,3],[0,-2,0],[-3,-3,-3]]样例中发生错误。
错误的原因我对每一个i、j,都优先求最小的dp[i][j], memo[i][j]跟随dp[i][j]最小的情况。可是由dp[i][j]递推到dp[i + 1][j]的时候,dp[i + 1][j]不仅与dp[i][j]有关,还与memo[i][j]有关。
也就是说,如果设x为公主在i、j时,骑士救到公主,在他走过的所有路径中,每一条路径耗费的最高健康指数。y为骑士通过所有可能的路径来到i、j时,其当前体力。存在(x, y),x比dp[i][j]大,y比memo[i][j]小,并且在由i、j递推到dp[i + 1][j]时,所得的结果优于(dp[i][j], memo[i][j])。