Algorithm

Dungeon Game

Dynamic Programming Pattern

Dungeon Game

A knight starts in the top-left cell of an m × n dungeon and must reach the princess in the bottom-right cell, moving only right or down. Each cell holds an integer that is added to the knight's health on entry: negative numbers are damage, positive numbers are healing, zero is empty. The knight dies the instant his health drops to 0 or below, at any point including the first and last cells. Return the minimum starting health that allows him to complete some path alive.

CONSTRAINTS
  • 1 ≤ m, n ≤ 200
  • -1000 ≤ dungeon[i][j] ≤ 1000
  • Movement is right or down only
  • Health must stay at 1 or above at every cell, including the starting and final cells
EXAMPLE 1
Input: dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
Output: 7
Travelling right, right, down, down, the knight's health from a start of 7 goes 5, 2, 5, 6, 1 — never reaching zero. Starting with 6 would leave him at exactly 0 on the final cell, which counts as death.
EXAMPLE 2
Input: dungeon = [[0]]
Output: 1
A single empty room changes nothing, but the knight must still be alive inside it, so he needs at least 1 health.
EXAMPLE 3
Input: dungeon = [[100]]
Output: 1
A large potion cannot reduce the requirement below 1, because he must already be alive when he enters the room to drink it.
EXAMPLE 4
Input: dungeon = [[-5]]
Output: 6
Entering with 6 leaves 1 after taking 5 damage. Entering with 5 would leave 0, which is death.
EXAMPLE 5
Input: dungeon = [[1,-3,3],[0,-2,0],[-3,-3,-3]]
Output: 3
Travelling right, right, down, down from a start of 3, the health reads 4, 1, 4, 4, 1 — it dips to 1 twice but never reaches 0. Routes through the bottom row meet three separate -3 rooms and demand far more.
Does the knight die at 0 health, or only below 0?
At 0. He must hold at least 1 at every moment, which is exactly why the recurrence clamps every requirement to a minimum of 1. Assuming he survives at 0 gives an answer that is one too small on many inputs.
Does the cell value apply on entering the cell, including the very first one?
Yes, both the starting cell and the princess's cell apply their values, and he must be alive after each. A negative starting cell therefore raises the required starting health directly.
Can he move up or left to collect a potion and come back?
No, only right and down. If backtracking were allowed the problem would change completely, since a positive-value cell could be farmed repeatedly.
Is the answer the health at the start of the journey, before entering the first room?
Yes — the number returned is what he needs in hand as he steps into the top-left cell. Being precise about this is what keeps the base case at the princess's cell correct.
Can I solve this by finding the path with the largest total and then checking it?
No, and it is worth saying why. Survival depends on the lowest point along the path, not on the total, and the path with the best total can have a deeper dip than a path with a worse total. That mismatch is exactly why the problem must be worked backwards from the destination.
Why this problem is genuinely different

It looks like every other grid problem: right and down, add up the cells, optimise. It is not, and the reason is worth understanding deeply, because it teaches the most important rule about choosing a state.

We want the minimum starting health. Health must never touch zero — not at the end of the journey, at every single cell along the way. So a path is not scored by its total; it is scored by its worst moment.

The natural approach, and exactly where it breaks

The instinct is to do what we always do: sweep forward from the top-left, storing at each cell something like "the best health I could have arriving here". Let us try to make that work and watch it fail.

Take this dungeon:

 -2  -3   3
 -5 -10   1
 10  30  -5

Consider two routes to the cell containing 1 at position (1,2):

- Right, right, down: −2, −3, 3, 1. Total damage −1.
- Down, down, right, right is a different route; but stay with two comparable ones. Route A through the top: −2, −3, 3 then down to 1 — running total −1, and the worst dip along the way was −5 (after −2 and −3).
- Route B: −2, −5, −10, 1 — running total −16, worse in total and worse in dip.

That comparison was easy. Now the problem. Suppose two routes arrive at the same cell where route A has a better running total but suffered a worse dip, while route B has a worse total but never dipped much. Which one should we keep? Neither dominates. Keeping the better total can be wrong, because the dip might have required an enormous starting health. Keeping the smaller dip can be wrong, because the poorer total will demand more health later.

That means "best health arriving here" is not a valid state at all. Two paths arriving at the same cell do not have the same future requirement, so the cell alone does not determine what comes next — and that is precisely the condition a state must satisfy. You could patch it by carrying both the current health and the worst dip so far, but then the state has a numeric dimension whose range is enormous, and the table becomes unbuildable.

Key Insightwhen a forward state does not work, try reversing the direction of the question. Here, instead of asking "what health do I have when I arrive?", ask "what health do I need when I arrive?" That flips the dependency, and the flipped version is a valid state — because the health you need from a cell onward genuinely depends on nothing but that cell.
The backward state

State Definition: f(r, c) is the minimum health the knight must have at the moment he enters cell (r, c) in order to survive from there to the princess. By definition this is at least 1, since he must be alive on arrival.

Now derive the recurrence. Suppose he enters (r, c) with health h. Entering applies the cell's value, so afterwards he has h + dungeon[r][c]. He then steps to whichever neighbour he chooses, and to survive he must arrive there with at least that neighbour's requirement. He will naturally pick the cheaper neighbour, so let

need_next = min( f(r+1, c), f(r, c+1) )

Then he needs h + dungeon[r][c] >= need_next, which rearranges to

h >= need_next - dungeon[r][c]

And on top of that he must be alive on entry, so h >= 1. Putting both together:

f(r, c) = max( 1, need_next - dungeon[r][c] )

That max(1, ...) is doing real work. If the cell is a big health potion, the requirement computed by the subtraction can go to zero or negative — meaning "you could have arrived on fumes". But you cannot arrive with zero health, because you would already be dead. Clamping to 1 encodes that rule exactly.

Base case: at the princess's cell (m-1, n-1) there is no next step, so he only needs to survive entering it: f(m-1, n-1) = max(1, 1 - dungeon[m-1][n-1]). If the final cell is +5 he needs just 1; if it is −5 he needs 6.

The answer is f(0, 0).

Building it
python
def calculate_minimum_hp(dungeon):
    m, n = len(dungeon), len(dungeon[0])
    INF = float('inf')
    memo = {}

    def f(r, c):
        if r >= m or c >= n: return INF                 # off the grid
        if r == m - 1 and c == n - 1:
            return max(1, 1 - dungeon[r][c])
        if (r, c) in memo: return memo[(r, c)]
        need_next = min(f(r + 1, c), f(r, c + 1))
        memo[(r, c)] = max(1, need_next - dungeon[r][c])
        return memo[(r, c)]

    return f(0, 0)

Now the table. Let dp[r][c] hold what f(r, c) returned — the health needed on entering (r, c). Swap the calls for reads:

dp[r][c] = max( 1, min(dp[r+1][c], dp[r][c+1]) - dungeon[r][c] )

Each cell reads the cell below and the cell to its right, both at larger indices. So fill from the bottom-right corner backwards — rows bottom-to-top, columns right-to-left — and both neighbours are ready when you reach (r, c). The base case is folded in with sentinels, explained just below the code. The answer is dp[0][0]:

python
def calculate_minimum_hp(dungeon):
    m, n = len(dungeon), len(dungeon[0])
    INF = float('inf')

    # one extra row and column of INF so the corner needs no special case
    dp = [[INF] * (n + 1) for _ in range(m + 1)]
    dp[m][n - 1] = 1          # stepping "past" the princess downward costs nothing extra
    dp[m - 1][n] = 1          # ... nor rightward

    for r in range(m - 1, -1, -1):
        for c in range(n - 1, -1, -1):
            need_next = min(dp[r + 1][c], dp[r][c + 1])
            dp[r][c] = max(1, need_next - dungeon[r][c])

    return dp[0][0]

The two sentinel cells set to 1 are a neat trick: they make the princess's cell obey the same formula as every other cell, since min(1, INF) = 1 gives max(1, 1 - dungeon[last]) automatically. Everything else off the grid stays infinite and is never chosen.

One row

Each cell reads the cell below and the cell to its right, so a single array swept right-to-left works: when you compute position c, the array still holds the row below at index c, and already holds this row's value at index c+1.

python
def calculate_minimum_hp(dungeon):
    m, n = len(dungeon), len(dungeon[0])
    INF = float('inf')
    dp = [INF] * (n + 1)
    dp[n - 1] = 1                                # sentinel for the princess's row

    for r in range(m - 1, -1, -1):
        for c in range(n - 1, -1, -1):
            need_next = min(dp[c], dp[c + 1])    # dp[c] is still the row below
            dp[c] = max(1, need_next - dungeon[r][c])
        dp[n] = INF                              # keep the right-hand sentinel clean

    return dp[0]
Worked Example:[[-2,-3,3],[-5,-10,1],[10,30,-5]]
Fill from the bottom-right corner.

Cell (2,2) = -5: max(1, 1 - (-5)) = 6. He must enter the princess's cell with 6.
Cell (2,1) = 30: the only next step is right, needing 6. max(1, 6 - 30) = max(1, -24) = 1. The potion is huge, so 1 health suffices — clamped, as promised.
Cell (2,0) = 10: next is (2,1) needing 1. max(1, 1 - 10) = 1.
Cell (1,2) = 1: the only next step is down to (2,2) needing 6. max(1, 6 - 1) = 5.
Cell (1,1) = -10: next is min(down (2,1)=1, right (1,2)=5) = 1. max(1, 1 + 10) = 11.
Cell (1,0) = -5: next is min(down (2,0)=1, right (1,1)=11) = 1. max(1, 1 + 5) = 6.
Cell (0,2) = 3: next is down (1,2)=5. max(1, 5 - 3) = 2.
Cell (0,1) = -3: next is min(down (1,1)=11, right (0,2)=2) = 2. max(1, 2 + 3) = 5.
Cell (0,0) = -2: next is min(down (1,0)=6, right (0,1)=5) = 5. max(1, 5 + 2) = 7.

Answer 7. Verify it forwards along the chosen route right, right, down, down: start 7 → enter −2 leaves 5 → enter −3 leaves 2 → enter 3 leaves 5 → enter 1 leaves 6 → enter −5 leaves 1. Alive at every step, and starting with 6 would have died on the final cell. The backward table and the forward simulation agree.

The lesson, which outlasts the problem

This is the page to remember when a table refuses to work.

The rule a state must satisfy is: once you know the state, the rest of the problem must not care how you got there. Whenever you can construct two histories that reach the same cell but face genuinely different futures, your state is wrong. You then have exactly two options — add whatever is missing to the state, or change the direction of the question so the missing information stops mattering.

Here, adding to the state was impractical, and reversing worked perfectly. That reversal is a repeatable move, and its signature is easy to spot: the objective involves a constraint that must hold at every step, rather than only at the end. Minimum required resource, minimum initial fuel, latest safe departure time — all of these are naturally computed backwards from the goal, because a requirement propagates backwards while an accumulation propagates forwards.

The deciding factor is not the grid; it is whether the thing you are optimising is a total or a worst case. A plain sum can be minimised forward or backward — either is fine. A constraint that must hold at every single step, like survival here, is what forces the backward direction.

Exponential Try Every Path
O(M × N) Time · O(M × N) Space Backward Memoisation
O(M × N) Time · O(M × N) Space Tabulation
O(M × N) Time · O(N) Space One Row