算法-动态规划-地下城游戏
1 题目概述
1.1 题目出处
https://leetcode-cn.com/problems/dungeon-game/
1.2 题目描述
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
说明:
骑士的健康点数没有上限。
任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
2 顺序遍历
2.1 思路
从左上开始按列、行开始遍历。
但是这个算法有问题,就是不能凭借左侧和上方的剩余血量和最小血量来评估应该选哪条路径。
2.2 代码
未完成。。。
思路有问题
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int result = 0;
if(dungeon == null | dungeon.length == 0){
return result;
}
int m = dungeon.length;
int n = dungeon[0].length;
Element[][] dp = new Element[m][n];
for(int i = 0; i < m; i++){
for(int i = 0; j < n; j++){
if(i + j == 0){
if(dungeon[0][0] < 0){
dp[0][0] = new Element(0 - dungeon[0][0], 1);
}else{
dp[0][0] = new Element(1, 1 + dungeon[0][0]);
}
} else if(i == 0){
if(dungeon[0][j] < 0){
dp[0][j] = new Element(0 - dungeon[0][j] + dp[0][j-1].min, 1);
}else{
dp[0][j] = new Element(1, 1 + dungeon[0][j].cur);
}
} else if(j == 0){
if(dungeon[i][0] < 0){
dp[i][0] = new Element(0 - dungeon[i][0] + dp[i-1][0].min, 1);
}else{
dp[i][0] = new Element(1, 1 + dungeon[i][0].cur);
}
} else{
if(dungeon[i][j] < 0){
dp[i][0] = new Element(0 - dungeon[i][0] + dp[i-1][0].min, 1);
}else{
if(dp[i-1][j].min < dp[i][j-1].min){
dp[i][j] = new Element(0 - dungeon[i][0] + dp[i-1][0].min, 1);
}
}
}
}
}
}
class Element{
public int min;
public int cur;
public Element(int min, int cur){
this.min = min;
this.cur = cur;
}
}
}
3 动态规划-倒序
3.1 思路
从右下角开始,倒序按列、行顺序倒推,以dp[i][j]表示到达该位置时的最小生命值。每个房间最低健康值,受制于以下条件:
- 本房间增加或减少生命值
- 右侧房间和下侧房间需要的最低生命值的最小值(因为我们求的就是最低生命值路径)
则状态转移方程如下:
if(dungeon[i][j] < 0){
dp[i][j] = 0 - dungeon[i][j] + Math.min(dp[i + 1][j], dp[i][j + 1]);
}else{
int min = Math.min(dp[i + 1][j], dp[i][j + 1]);
if (dungeon[i][j] + 1 >= min){
dp[i][j] = 1;
}else{
dp[i][j] = min - dungeon[i][j];
}
}
3.2 代码
3.2.1 初版-较冗长
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int result = 0;
if(dungeon == null | dungeon.length == 0){
return result;
}
int m = dungeon.length;
int n = dungeon[0].length;
int[][] dp = new int[m][n];
for(int i = m -1; i >= 0; i--){
for(int j = n -1; j >= 0; j--){
if(i == (m-1) && j == (n-1)){
if(dungeon[i][j] < 0){
dp[i][j] = 0 - dungeon[i][j] + 1;
}else{
dp[i][j] = 1;
}
} else if(i == (m-1)){
if(dungeon[i][j] < 0){
dp[i][j] = 0 - dungeon[i][j] + dp[i][j + 1];
}else{
if (dungeon[i][j] + 1 >= dp[i][j + 1]){
dp[i][j] = 1;
}else{
dp[i][j] = dp[i][j + 1] - dungeon[i][j];
}
}
} else if(j == (n-1)){
if(dungeon[i][j] < 0){
dp[i][j] = 0 - dungeon[i][j] + dp[i + 1][j] ;
}else{
if (dungeon[i][j] + 1 >= dp[i + 1][j]){
dp[i][j] = 1;
}else{
dp[i][j] = dp[i + 1][j] - dungeon[i][j];
}
}
} else{
if(dungeon[i][j] < 0){
dp[i][j] = 0 - dungeon[i][j] + Math.min(dp[i + 1][j], dp[i][j + 1]);
}else{
int min = Math.min(dp[i + 1][j], dp[i][j + 1]);
if (dungeon[i][j] + 1 >= min){
dp[i][j] = 1;
}else{
dp[i][j] = min - dungeon[i][j];
}
}
}
}
}
return dp[0][0];
}
}
3.2.2 优化版
抽象出min
, 表示受到本身(至少为1)、右侧、下侧的最低生命值约束的最低生命值
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int result = 0;
if(dungeon == null | dungeon.length == 0){
return result;
}
int m = dungeon.length;
int n = dungeon[0].length;
// 用来记录到达每个元素时至少需要的生命值
int[][] dp = new int[m][n];
// 从右下往左上开始反推
for(int i = m -1; i >= 0; i--){
for(int j = n -1; j >= 0; j--){
// 表示受到本身(至少为1)、右侧、下侧的最低生命值约束的最低生命值
int min = 0;
if(i == (m-1) && j == (n-1)){
// 最右下角节点,因为他不需要考虑右侧和下侧,只受到每个节点健康值至少为1否则死亡的约束
min = 1;
} else if(i == (m-1)){
// 若是考虑最后一行,则只需要考虑右侧的最低生命值
min = dp[i][j + 1];
} else if(j == (n-1)){
// 若是考虑最后一列,则只需要考虑下侧的最低生命值
min = dp[i + 1][j];
} else{
// 其他情况,需要考虑下侧和右侧的最低生命值
min = Math.min(dp[i + 1][j], dp[i][j + 1]);
}
if(dungeon[i][j] < 0){
// 如果本房间会降低生命值(为负值),则最低生命值为(抵消该负值 + 需要考虑的后续最低生命值)
dp[i][j] = 0 - dungeon[i][j] + min;
}else{
// 如果本房间会增加生命值(为正值),则需要分两种情况考虑
if (dungeon[i][j] + 1 >= min){
// 如果本房间增加生命值 + 至少为1的健康值 大于等于 需要考虑的后续最低生命值
// 说明到达本节点时生命值为1就够了
dp[i][j] = 1;
}else{
// 否则本房间增加生命值 + 至少为1的健康值 小于 需要考虑的后续最低生命值
// 则还需要 需要考虑的后续最低生命值 - 本房间增加生命值 作为到达本节点的最低生命值
dp[i][j] = min - dungeon[i][j];
}
}
}
}
return dp[0][0];
}
}
3.2.3 优化版2
在dp
最右列外和最下行外虚拟一圈节点,除了真实最右下节点右侧和下侧的最低生命值位1以外,其他都为Integer.MAX_VALUE
,这样做的好处是循环中不需要特殊处理最后一行、最后一列和右下了。
同时,还统一了房间增加健康值和减去健康值的处理逻辑,代码更简洁。
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int result = 0;
if(dungeon == null | dungeon.length == 0){
return result;
}
int m = dungeon.length;
int n = dungeon[0].length;
// 用来记录到达每个元素时至少需要的生命值
int[][] dp = new int[m+1][n+1];
// 在最右列外和最下行外虚拟一圈节点,除了真实最右下节点右侧和下侧的最低生命值位1以外
// 其他都为Integer.MAX_VALUE,这样做的好处是循环中不需要特殊处理最后一行、最后一列和右下了
dp[m - 1][n] = 1;
dp[m][n - 1] = 1;
for(int i = 0; i < m-1; i++){
dp[i][n] = Integer.MAX_VALUE;
}
for(int i = 0; i < n-1; i++){
dp[m][i] = Integer.MAX_VALUE;
}
// 从右下往左上开始反推
for(int i = m - 1; i >= 0; i--){
for(int j = n - 1; j >= 0; j--){
// 受到本身(至少为1)、右侧、下侧的最低生命值约束的最低生命值
dp[i][j] = Math.max(Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j], 1);
}
}
return dp[0][0];
}
}
3.3 时间复杂度
- 初版
- 优化版
O(M*N)
3.4 空间复杂度
O(M*N)
4 DFS
4.1 思路
从左上角开始进行DFS,每个房间最低健康值,受制于以下条件:
- 本房间增加或减少生命值
- 右侧房间和下侧房间需要的最低生命值的最小值(因为我们求的就是最低生命值路径)
4.2 代码
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int result = 0;
if(dungeon == null | dungeon.length == 0){
return result;
}
int m = dungeon.length;
int n = dungeon[0].length;
// 用来记录到达每个元素时至少需要的生命值
int[][] dp = new int[m+1][n+1];
// 在最右列外和最下行外虚拟一圈节点,除了真实最右下节点右侧和下侧的最低生命值位1以外
// 其他都为Integer.MAX_VALUE,这样做的好处是循环中不需要特殊处理最后一行、最后一列和右下了
dp[m - 1][n] = 1;
dp[m][n - 1] = 1;
for(int i = 0; i < m-1; i++){
dp[i][n] = Integer.MAX_VALUE;
}
for(int i = 0; i < n-1; i++){
dp[m][i] = Integer.MAX_VALUE;
}
return dfs(dungeon, dp, 0, 0);
}
private int dfs(int[][] dungeon, int[][] dp, int i, int j){
dp[i][j] = dp[i][j] > 0 ? dp[i][j] : Math.max(1, Math.min(dfs(dungeon, dp, i + 1, j), dfs(dungeon, dp, i, j + 1)) - dungeon[i][j]);
return dp[i][j];
}
}
4.3 时间复杂度
O(M*N)
4.4 空间复杂度
O(M*N)