1. 程式人生 > >leetcode | 174. Dungeon Game

leetcode | 174. Dungeon Game

題目

The demons had captured the princess § and imprisoned her in the bottom-right corner of a dungeon. The dungeon consists of M x N rooms laid out in a 2D grid. Our valiant knight (K) was initially positioned in the top-left room and must fight his way through the dungeon to rescue the princess.

The knight has an initial health point represented by a positive integer. If at any point his health point drops to 0 or below, he dies immediately.

Some of the rooms are guarded by demons, so the knight loses health (negative integers) upon entering these rooms; other rooms are either empty (0’s) or contain magic orbs that increase the knight’s health (positive integers).

In order to reach the princess as quickly as possible, the knight decides to move only rightward or downward in each step.

Write a function to determine the knight’s minimum initial health so that he is able to rescue the princess.

For example, given the dungeon below, the initial health of the knight must be at least 7 if he follows the optimal path RIGHT-> RIGHT -> DOWN -> DOWN.

-2 (K) -3 3
-5 -10 1
10 30 -5 (P)

Note:

  • The knight’s health has no upper bound.
  • Any room can contain threats or power-ups, even the first room the knight enters and the bottom-right room where the princess is imprisoned.

思路與解法

這道題題目是讓我們選擇一條從左上角K走到右下叫P的路徑,使得經此路徑騎士所需的最初血量最少。乍一看,這道題目與64. Minimum Path Sum似乎有些類似,都是從地圖左上角走到右下角,找到一條最佳路徑使得所需代價最少。

錯誤的思路

直觀上,我們會得到以下想法:
對於節點(i,j),當(i-1,j)所需最初血量更少時,選擇(i-1,j),否則選擇(i,j-1);如果兩者相同,則選擇(i-1,j)(i,j-1)中當前所剩血量更大的節點。
但是,這道題目不能用以上的思路,節點(i,j)的代價值雖然與(i-1,j)、(i,j-1)兩個節點直接相關,但該節點其實也受到後續路徑的影響,我們所得到的僅僅是區域性最優解而已。以一個例子說明此現象:節點下標範圍i ∈[0,rows-1],j∈[0, cols-1]

1K -3 3
0 -2 0
-3 -3 -3(P)

對於節點(1,2),其直接代價來源為(0,2)(1,1);此時我們假設騎士最初的血量為0,則從(0,0)移動到(0,2)過程中,有如下表格:

節點 (0,0) (0,1) (0,2)
血量 1 -2 1
最初所需最小血量 0 2 2

(0,0)移動到(1,1)過程中,有如下表格:

節點 (0,0) (1,0) (1,1)
血量 1 1 -1
最初所需最小血量 0 0 1

右上述兩條路徑可知,到達(1,1)時所需的最初血量更少,所以,節點(1,2)會選擇節點(1,1),此時,到達節點(2,2)所需最初血量為5。但實際上,如果節點(1,2)選擇節點(0,2),則到達節點(2,2)所需最初血量為3。所以我們最初的想法並不成立。

正確的思路

由於當前節點的選擇收到後續路徑的影響,所以我們可以採取由後向前的想法來解決,即從終點向起點進行遞推。
定義dp[i][j]表示從節點(i,j)到達終點P所需的最小初始血量。則存在以下狀態轉移方程:

// 一般節點(並不納入邊界點)
// Right
if dungeon[i][j] > dp[i+1][j] {
	dp[i][j] = 0
} else {
	dp[i][j] = dp[i+1][j] - dungeon[i][j]
}

// Down
if dungeon[i][j] > dp[i][j+1] {
	dp[i][j] = min(0, dp[i][j])
} else {
	dp[i][j] = min(dp[i][j+1]-dungeon[i][j], dp[i][j])
}

最終返回結果為dp[0][0]+1,原因是血量必須大於0。

程式碼實現

const INT_MIN = ^int(^uint(0) >> 1)
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
func calculateMinimumHP(dungeon [][]int) int {
	rows := len(dungeon)
	cols := len(dungeon[0])
	dp := make([][]int, rows)
	for i := 0; i < rows; i++ {
		dp[i] = make([]int, cols)
	}
	// 右下角
	if dungeon[rows-1][cols-1] > 0 {
		dp[rows-1][cols-1] = 0
	} else {
		dp[rows-1][cols-1] = -dungeon[rows-1][cols-1]
	}
	// 邊界-右
	for i := rows - 2; i >= 0; i-- {
		if dungeon[i][cols-1] > dp[i+1][cols-1] {
			dp[i][cols-1] = 0
		} else {
			dp[i][cols-1] = dp[i+1][cols-1] - dungeon[i][cols-1]
		}
	}
	// 邊界-下
	for j := cols - 2; j >= 0; j-- {
		if dungeon[rows-1][j] > dp[rows-1][j+1] {
			dp[rows-1][j] = 0
		} else {
			dp[rows-1][j] = dp[rows-1][j+1] - dungeon[rows-1][j]
		}
	}
	for i := rows - 2; i >= 0; i-- {
		for j := cols - 2; j >= 0; j-- {
			// Right
			if dungeon[i][j] > dp[i+1][j] {
				dp[i][j] = 0
			} else {
				dp[i][j] = dp[i+1][j] - dungeon[i][j]
			}
			// Down
			if dungeon[i][j] > dp[i][j+1] {
				dp[i][j] = min(0, dp[i][j])
			} else {
				dp[i][j] = min(dp[i][j+1]-dungeon[i][j], dp[i][j])
			}
		}
	}
	return dp[0][0] + 1
}

時間複雜度分析

有上述程式碼可知,時間複雜度為O(M*N)。執行結果如下:
在這裡插入圖片描述