Facebook Pixel

174. Dungeon Game

Problem Description

You have a 2D grid representing a dungeon with m x n rooms. A knight starts at the top-left corner and needs to rescue a princess imprisoned in the bottom-right corner.

The knight begins with some amount of health points (a positive integer). If his health ever drops to 0 or below, he dies immediately.

Each room in the dungeon contains a value:

  • Negative values represent demons that damage the knight (reduce health by that amount)
  • Zero represents an empty room (no health change)
  • Positive values represent magic orbs that heal the knight (increase health by that amount)

The knight can only move right or down in each step to reach the princess as quickly as possible.

Your task is to find the minimum initial health the knight must have to successfully reach the princess while keeping his health above 0 throughout the entire journey.

For example, if the dungeon is:

[[-3, 5],
 [1, -4]]

The knight would need at least 2 initial health points to survive the path: start → right → down → princess.

Important notes:

  • The knight must have at least 1 health point at all times (including after reaching the princess)
  • Even the starting room (top-left) and ending room (bottom-right) can contain threats or power-ups
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to work backwards from the destination to the starting point. Why? Because if we try to work forward, we'd need to keep track of all possible paths and their minimum health requirements, which becomes complex.

Think about it this way: when the knight reaches any cell, he needs enough health to:

  1. Survive that cell's effect (demon damage or healing)
  2. Have enough health to complete the rest of the journey to the princess

By working backwards, we can answer: "What's the minimum health needed when entering this cell to successfully reach the princess?"

Let's consider the princess's cell first. After dealing with whatever is in that room, the knight needs at least 1 health to survive. So if the princess's room has a demon dealing 5 damage, the knight needs at least 6 health when entering that room.

For any other cell (i, j), the knight will move either right to (i, j+1) or down to (i+1, j). Since the knight wants to minimize initial health, he'll choose the path that requires less health. Therefore:

  • We need to know the minimum health required at (i, j+1) and (i+1, j)
  • The knight will choose the path with the smaller requirement
  • Then we account for the current cell's effect

The formula becomes: dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j])

The max(1, ...) ensures the knight always has at least 1 health. The subtraction works because:

  • If the current cell has a demon (-5), we need MORE initial health: required - (-5) = required + 5
  • If the current cell has healing (+5), we need LESS initial health: required - 5

This backward dynamic programming approach elegantly solves the problem in O(m*n) time.

Solution Approach

We implement the backward dynamic programming approach using a 2D array dp where dp[i][j] represents the minimum health required when entering cell (i, j) to successfully reach the princess.

Step 1: Initialize the DP table

Create a (m+1) x (n+1) table filled with infinity values. The extra row and column serve as boundaries to handle edge cases elegantly.

dp = [[inf] * (n + 1) for _ in range(m + 1)]

Step 2: Set base cases

Since we work backwards, we need to set the values for cells adjacent to the princess's cell (m-1, n-1):

  • dp[m][n-1] = 1: Virtual cell below the princess
  • dp[m-1][n] = 1: Virtual cell to the right of the princess

These ensure that after dealing with the princess's cell, the knight has at least 1 health.

dp[m][n - 1] = dp[m - 1][n] = 1

Step 3: Fill the DP table backwards

Iterate from bottom-right to top-left. For each cell (i, j), calculate the minimum health needed:

for i in range(m - 1, -1, -1):
    for j in range(n - 1, -1, -1):
        dp[i][j] = max(1, min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j])

The formula max(1, min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j]) works as follows:

  • min(dp[i+1][j], dp[i][j+1]): Choose the path requiring less health (either go down or right)
  • - dungeon[i][j]: Adjust for the current cell's effect
    • If dungeon[i][j] is negative (demon), this increases the required health
    • If dungeon[i][j] is positive (healing), this decreases the required health
  • max(1, ...): Ensure the knight always has at least 1 health point

Step 4: Return the result

dp[0][0] contains the minimum initial health required at the starting position.

return dp[0][0]

The algorithm runs in O(m*n) time complexity and uses O(m*n) space for the DP table.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Consider this dungeon:

[[-3, 5],
 [1, -4]]

We need to find the minimum initial health for the knight to reach from top-left (0,0) to bottom-right (1,1).

Step 1: Initialize the DP table

We create a 3x3 DP table (one extra row and column for boundaries):

dp = [[inf, inf, inf],
      [inf, inf, inf],
      [inf, inf, inf]]

Step 2: Set base cases

We set the cells adjacent to the princess's position (virtual boundaries):

dp[2][1] = 1  (below princess)
dp[1][2] = 1  (right of princess)

dp = [[inf, inf, inf],
      [inf, inf,  1 ],
      [inf,  1,  inf]]

Step 3: Fill the DP table backwards

Starting from the princess's cell (1,1) where dungeon[1][1] = -4:

  • dp[1][1] = max(1, min(dp[2][1], dp[1][2]) - dungeon[1][1])
  • dp[1][1] = max(1, min(1, 1) - (-4))
  • dp[1][1] = max(1, 1 + 4) = 5

The knight needs 5 health entering this room to survive the 4 damage and have 1 health left.

Next, cell (1,0) where dungeon[1][0] = 1:

  • dp[1][0] = max(1, min(dp[2][0], dp[1][1]) - dungeon[1][0])
  • dp[1][0] = max(1, min(inf, 5) - 1)
  • dp[1][0] = max(1, 4) = 4

The healing orb reduces the requirement from 5 to 4.

Cell (0,1) where dungeon[0][1] = 5:

  • dp[0][1] = max(1, min(dp[1][1], dp[0][2]) - dungeon[0][1])
  • dp[0][1] = max(1, min(5, inf) - 5)
  • dp[0][1] = max(1, 0) = 1

The strong healing orb means we only need 1 health to enter this cell.

Finally, cell (0,0) where dungeon[0][0] = -3:

  • dp[0][0] = max(1, min(dp[1][0], dp[0][1]) - dungeon[0][0])
  • dp[0][0] = max(1, min(4, 1) - (-3))
  • dp[0][0] = max(1, 1 + 3) = 4

The demon in the starting room requires 4 initial health.

Final DP table:

dp = [[ 4,  1, inf],
      [ 4,  5,  1 ],
      [inf, 1, inf]]

Step 4: Return the result

dp[0][0] = 4 is our answer. The knight needs at least 4 initial health points.

Let's verify with the optimal path (right → down):

  • Start at (0,0) with 4 health, take 3 damage → 1 health
  • Move right to (0,1), gain 5 health → 6 health
  • Move down to (1,1), take 4 damage → 2 health
  • Princess rescued with 2 health remaining!

Solution Implementation

1class Solution:
2    def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
3        # Get dimensions of the dungeon
4        rows, cols = len(dungeon), len(dungeon[0])
5      
6        # Initialize DP table with infinity values
7        # Extra row and column for boundary conditions
8        dp = [[float('inf')] * (cols + 1) for _ in range(rows + 1)]
9      
10        # Set base cases: minimum health needed just outside the princess room is 1
11        dp[rows][cols - 1] = dp[rows - 1][cols] = 1
12      
13        # Fill DP table from bottom-right to top-left
14        for row in range(rows - 1, -1, -1):
15            for col in range(cols - 1, -1, -1):
16                # Minimum health needed at current cell is:
17                # - At least 1 (knight must survive)
18                # - Minimum health from next cell minus current cell's value
19                min_health_on_exit = min(dp[row + 1][col], dp[row][col + 1])
20                dp[row][col] = max(1, min_health_on_exit - dungeon[row][col])
21      
22        # Return minimum initial health needed at starting position
23        return dp[0][0]
24
1class Solution {
2    public int calculateMinimumHP(int[][] dungeon) {
3        // Get dimensions of the dungeon
4        int rows = dungeon.length;
5        int cols = dungeon[0].length;
6      
7        // Create DP table with extra row and column for boundary conditions
8        // dp[i][j] represents minimum health needed to reach bottom-right from position (i,j)
9        int[][] dp = new int[rows + 1][cols + 1];
10      
11        // Initialize all cells with a large value (acts as infinity)
12        for (int[] row : dp) {
13            Arrays.fill(row, Integer.MAX_VALUE);
14        }
15      
16        // Base case: Set boundary cells adjacent to destination
17        // Knight needs at least 1 HP to survive after reaching the princess
18        dp[rows][cols - 1] = 1;  // Cell below the destination
19        dp[rows - 1][cols] = 1;  // Cell to the right of the destination
20      
21        // Fill the DP table from bottom-right to top-left
22        for (int i = rows - 1; i >= 0; i--) {
23            for (int j = cols - 1; j >= 0; j--) {
24                // Calculate minimum health needed at current cell
25                // We need enough health to survive current cell and reach the next cell
26                int minHealthFromNext = Math.min(dp[i + 1][j], dp[i][j + 1]);
27              
28                // If current cell has positive value (potion), it reduces required health
29                // If current cell has negative value (damage), it increases required health
30                // Minimum health must be at least 1
31                dp[i][j] = Math.max(1, minHealthFromNext - dungeon[i][j]);
32            }
33        }
34      
35        // Return minimum initial health needed to start from top-left corner
36        return dp[0][0];
37    }
38}
39
1class Solution {
2public:
3    int calculateMinimumHP(vector<vector<int>>& dungeon) {
4        int rows = dungeon.size();
5        int cols = dungeon[0].size();
6      
7        // Create DP table with extra row and column for boundary conditions
8        // dp[i][j] represents minimum health required to reach bottom-right from cell (i,j)
9        int dp[rows + 1][cols + 1];
10      
11        // Initialize all cells with a large value (infinity)
12        memset(dp, 0x3f, sizeof(dp));
13      
14        // Set boundary conditions: we need at least 1 HP to enter the princess cell
15        // These serve as base cases for cells adjacent to bottom-right corner
16        dp[rows][cols - 1] = 1;  // Cell below the bottom-right corner
17        dp[rows - 1][cols] = 1;  // Cell to the right of bottom-right corner
18      
19        // Fill the DP table from bottom-right to top-left
20        for (int i = rows - 1; i >= 0; --i) {
21            for (int j = cols - 1; j >= 0; --j) {
22                // Calculate minimum HP needed at current cell
23                // We can move either right (dp[i][j+1]) or down (dp[i+1][j])
24                int minHealthRequired = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
25              
26                // Health must be at least 1 to stay alive
27                dp[i][j] = max(1, minHealthRequired);
28            }
29        }
30      
31        // Return minimum initial HP required to start from top-left corner
32        return dp[0][0];
33    }
34};
35
1function calculateMinimumHP(dungeon: number[][]): number {
2    const rows: number = dungeon.length;
3    const cols: number = dungeon[0].length;
4  
5    // Create DP table with extra row and column for boundary conditions
6    // dp[i][j] represents minimum health required to reach bottom-right from cell (i,j)
7    const dp: number[][] = Array(rows + 1).fill(null).map(() => 
8        Array(cols + 1).fill(Number.MAX_SAFE_INTEGER)
9    );
10  
11    // Set boundary conditions: we need at least 1 HP to enter the princess cell
12    // These serve as base cases for cells adjacent to bottom-right corner
13    dp[rows][cols - 1] = 1;  // Cell below the bottom-right corner
14    dp[rows - 1][cols] = 1;  // Cell to the right of bottom-right corner
15  
16    // Fill the DP table from bottom-right to top-left
17    for (let i = rows - 1; i >= 0; i--) {
18        for (let j = cols - 1; j >= 0; j--) {
19            // Calculate minimum HP needed at current cell
20            // We can move either right (dp[i][j+1]) or down (dp[i+1][j])
21            const minHealthRequired: number = Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
22          
23            // Health must be at least 1 to stay alive
24            dp[i][j] = Math.max(1, minHealthRequired);
25        }
26    }
27  
28    // Return minimum initial HP required to start from top-left corner
29    return dp[0][0];
30}
31

Time and Space Complexity

The time complexity is O(m × n), where m is the number of rows and n is the number of columns of the dungeon. This is because the algorithm uses nested loops that iterate through each cell of the dungeon exactly once. The outer loop runs m times (from m-1 to 0), and the inner loop runs n times (from n-1 to 0) for each iteration of the outer loop.

The space complexity is O(m × n). The algorithm creates a 2D DP table dp with dimensions (m+1) × (n+1) to store the minimum health required at each position. Since this auxiliary space grows proportionally with the size of the input dungeon, the space complexity is O((m+1) × (n+1)), which simplifies to O(m × n).

Common Pitfalls

1. Attempting Forward Dynamic Programming

Many people's first instinct is to use forward DP, tracking the minimum health needed as we traverse from top-left to bottom-right. This approach fails because we need to consider future threats when determining the minimum health at each position.

Why it fails: When moving forward, we don't know what dangers lie ahead. A cell with a large healing value might seem to allow very low health, but if the next cell has massive damage, we'd die.

Example:

dungeon = [[0, -5],
           [0,  0]]

Forward DP might suggest we need only 1 health at start since the first cell is empty. But we actually need 6 health to survive the -5 damage in cell (0,1).

Solution: Always use backward DP for this problem, starting from the destination and working backwards to determine minimum requirements.

2. Incorrect Boundary Initialization

A common mistake is initializing the boundary cells incorrectly or forgetting to set them properly.

Wrong approach:

dp[rows-1][cols] = 0  # Wrong! This would mean no health needed
dp[rows][cols-1] = 0  # Wrong!

Correct approach:

dp[rows-1][cols] = 1  # Knight needs at least 1 health after the princess
dp[rows][cols-1] = 1  # Knight needs at least 1 health after the princess

3. Forgetting the Minimum Health Constraint

Forgetting to enforce that the knight must always have at least 1 health point can lead to incorrect results.

Wrong calculation:

dp[row][col] = min(dp[row+1][col], dp[row][col+1]) - dungeon[row][col]
# This could result in 0 or negative values

Correct calculation:

dp[row][col] = max(1, min(dp[row+1][col], dp[row][col+1]) - dungeon[row][col])
# Ensures knight always has at least 1 health

4. Misunderstanding Health Calculation with Positive Values

When a room contains a healing orb (positive value), some might think it adds to the minimum required health. Actually, it reduces the health requirement for that path.

Example: If dungeon[i][j] = 5 (healing) and we need 10 health for the next cell, then:

  • We need max(1, 10 - 5) = 5 health when entering this cell
  • NOT 10 + 5 = 15 health

The healing orb helps us, so we need less initial health to survive the journey.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More