算法-動態規劃-地下城遊戲
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)