[LeetCode 174] Dungeon Game
The demons had captured the princess (P) 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.
To solve this problem, we can start from the grid's bottom-right corner. At this cell, we know that need to maintain a min health of 1. Using its cell value, we know the minimum health the knight must have before entering the current cell. There will be at most 2 options for the knight to choose: up or left. It is obvious he should choose the option that has a a lower minimum health point. To avoid over-lapping subproblems, we use dynamic programming here.
State: dp[i][j]: the minimum health required to go from (i, j) to (M - 1, N - 1).
State transition: if we have dp[i][j], we use it to update dp[i - 1][j] and dp[i][j - 1]. Take dp[i - 1][j] as an example, we have two scenarios:
1. dungeon[i - 1][j] >= 0: this means that in order to get dp[i][j], as long as we have dp[i][j] - dungeon[i - 1][j] health before entering (i - 1, j), we'll be good as dp[i][j] - dungeon[i - 1][j] + dungeon[i - 1][j] = dp[i][j]. There is one pitfall here though, dp[i][j] - dungeon[i - 1][j] can be <= 0 when dungeon[i - 1][j] is big positive integer. Because the knight must be at >= 1 health at any time, we have dp[i - 1][j] = Math.min(dp[i - 1][j], Math.max(1, dp[i][j] - dungeon[i - 1][j])).
2. dungeon[i - 1][j] < 0: this means that the knight must have at least dp[i][j] - dungeon[i - 1][j] health before entering (i - 1, j). This value is always >= 1.
Summing up the 2 cases, we have dp[i - 1][j] = Math.min(dp[i - 1][j], Math.max(1, dp[i][j] - dungeon[i - 1][j])). It is similar for dp[i][j - 1].
Initialization: dp[M - 1][N - 1] is 1 if dungeon[M - 1][N - 1] >= 0, 1 - dungeon[M - 1][N - 1] if dungeon[M - 1][N - 1] < 0.
Answer: dp[0][0].
More thinking: Can we solve this problem using the same dynamic programming approach, but instead of going backward, we go forward: start from (0, 0) and compute results for bigger index cell until we reach the bottom-right corner.
Normally we can for 2D grid DP problems, but not with this one. Consider the following example for a given cell(i, j).
For cell (i - 1, j), we have the current path sum 5, and the minimum health to reach from (0, 0) to (i - 1, j) as 6.
For cell(i, j - 1), we have the current path sum 41, and the minimum health to reach from (0, 0) to (i, j - 1) as 8.
If we choose the smaller minimum health, we'd go for cell(i - 1, j). But this will give us a sub-optimal answer. Say for the rest of the cells that the knight need to visit from (i, j) to (M - 1, N - 1), each cell has a value of -1. Because the option he chose (cell (i - 1, j) has a very small positive path sum), it means we need to keep increasing the minimum health to start with from(0, 0) by 1 each time he visits one more cell. 3 such operations will make this choice worse than choosing cell (i, j - 1) as the path sum of this choice provides a much bigger buffer zone. Thus, we actually do not have enough information to make the correct decision at cell(i, j) because this also depends on the values on the future grid cells that the knight hasn't visited yet. This is why we can not solve this problem starting from(0, 0)!
class Solution { public int calculateMinimumHP(int[][] dungeon) { int m = dungeon.length, n = dungeon[0].length; int[][] dp = new int[m][n]; for(int i = 0; i < dp.length; i++) { Arrays.fill(dp[i], Integer.MAX_VALUE); } if(dungeon[m - 1][n - 1] >= 0) { dp[m - 1][n - 1] = 1; } else { dp[m - 1][n - 1] = 1 - dungeon[m - 1][n - 1]; } for(int i = m - 1; i >= 0; i--) { for(int j = n - 1; j >= 0; j--) { if(i > 0) { dp[i - 1][j] = Math.min(dp[i - 1][j], Math.max(1, dp[i][j] - dungeon[i - 1][j])); } if(j > 0) { dp[i][j - 1] = Math.min(dp[i][j - 1], Math.max(1, dp[i][j] - dungeon[i][j - 1])); } } } return dp[0][0]; } }