🧠
Top-Down DP with Memoization
💡 Memoization avoids recomputing the same states multiple times, drastically improving efficiency while preserving the recursive intuition.
Intuition
Use a cache to store the minimum health needed for each cell so that repeated calls return instantly instead of recomputing.
Algorithm
- Initialize a memo dictionary or array to store results for each cell.
- Modify the recursive function to check the memo before computing.
- If the result is cached, return it immediately.
- Otherwise, compute as before, store the result in memo, and return it.
💡 Memoization transforms exponential recursion into polynomial time by caching results.
Recurrence:dp(i,j) = max(1, min(dp(i+1,j), dp(i,j+1)) - dungeon[i][j])
from typing import List
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
m, n = len(dungeon), len(dungeon[0])
memo = {}
def dfs(i, j):
if i == m or j == n:
return float('inf')
if i == m - 1 and j == n - 1:
return max(1, 1 - dungeon[i][j])
if (i, j) in memo:
return memo[(i, j)]
down = dfs(i + 1, j)
right = dfs(i, j + 1)
need = min(down, right) - dungeon[i][j]
memo[(i, j)] = max(1, need)
return memo[(i, j)]
return dfs(0, 0)
# Example usage:
# sol = Solution()
# print(sol.calculateMinimumHP([[-2,-3,3],[-5,-10,1],[10,30,-5]])) # Output: 7
Line Notes
memo = {}Initialize memo dictionary to cache results
if (i, j) in memo:Check if result for cell (i,j) is already computed
memo[(i, j)] = max(1, need)Store computed minimum health needed for cell (i,j)
return memo[(i, j)]Return cached result to avoid recomputation
def dfs(i, j):Defines recursive function to compute minimum health from cell (i,j)
if i == m or j == n:Out-of-bounds returns infinity to ignore invalid paths
if i == m - 1 and j == n - 1:Base case at princess cell, compute minimum health needed
down = dfs(i + 1, j)Recursive call moving down
right = dfs(i, j + 1)Recursive call moving right
need = min(down, right) - dungeon[i][j]Calculate health needed at current cell
import java.util.*;
public class Solution {
private int m, n;
private int[][] dungeon;
private Map<String, Integer> memo = new HashMap<>();
public int calculateMinimumHP(int[][] dungeon) {
this.dungeon = dungeon;
m = dungeon.length;
n = dungeon[0].length;
return dfs(0, 0);
}
private int dfs(int i, int j) {
if (i == m || j == n) return Integer.MAX_VALUE;
if (i == m - 1 && j == n - 1) return Math.max(1, 1 - dungeon[i][j]);
String key = i + "," + j;
if (memo.containsKey(key)) return memo.get(key);
int down = dfs(i + 1, j);
int right = dfs(i, j + 1);
int need = Math.min(down, right) - dungeon[i][j];
int res = Math.max(1, need);
memo.put(key, res);
return res;
}
// Example usage:
// public static void main(String[] args) {
// Solution sol = new Solution();
// int[][] dungeon = {{-2,-3,3},{-5,-10,1},{10,30,-5}};
// System.out.println(sol.calculateMinimumHP(dungeon)); // Output: 7
// }
}
Line Notes
private Map<String, Integer> memo = new HashMap<>();Memo map to cache results for each cell
String key = i + "," + j;Create unique key for cell (i,j) for memo lookup
if (memo.containsKey(key)) return memo.get(key);Return cached result if available
memo.put(key, res);Store computed result in memo
private int dfs(int i, int j)Recursive helper to compute minimum health from cell (i,j)
if (i == m || j == n) return Integer.MAX_VALUE;Out-of-bounds returns max int to ignore invalid paths
if (i == m - 1 && j == n - 1)Base case at princess cell, compute minimum health needed
int down = dfs(i + 1, j);Recursive call moving down
int right = dfs(i, j + 1);Recursive call moving right
int need = Math.min(down, right) - dungeon[i][j];Calculate health needed at current cell
#include <vector>
#include <unordered_map>
#include <string>
#include <algorithm>
#include <climits>
using namespace std;
class Solution {
public:
int m, n;
vector<vector<int>> dungeon;
unordered_map<string, int> memo;
string key(int i, int j) {
return to_string(i) + "," + to_string(j);
}
int dfs(int i, int j) {
if (i == m || j == n) return INT_MAX;
if (i == m - 1 && j == n - 1) return max(1, 1 - dungeon[i][j]);
string k = key(i, j);
if (memo.count(k)) return memo[k];
int down = dfs(i + 1, j);
int right = dfs(i, j + 1);
int need = min(down, right) - dungeon[i][j];
memo[k] = max(1, need);
return memo[k];
}
int calculateMinimumHP(vector<vector<int>>& dungeon_) {
dungeon = dungeon_;
m = dungeon.size();
n = dungeon[0].size();
return dfs(0, 0);
}
};
// Example usage:
// int main() {
// Solution sol;
// vector<vector<int>> dungeon = {{-2,-3,3},{-5,-10,1},{10,30,-5}};
// cout << sol.calculateMinimumHP(dungeon) << endl; // Output: 7
// return 0;
// }
Line Notes
unordered_map<string, int> memo;Memo map to cache results
string k = key(i, j);Generate unique key for cell (i,j)
if (memo.count(k)) return memo[k];Return cached result if present
memo[k] = max(1, need);Store computed minimum health needed
int dfs(int i, int j)Recursive function to compute minimum health from cell (i,j)
if (i == m || j == n) return INT_MAX;Out-of-bounds returns max int to ignore invalid paths
if (i == m - 1 && j == n - 1) return max(1, 1 - dungeon[i][j]);Base case at princess cell
int down = dfs(i + 1, j);Recursive call moving down
int right = dfs(i, j + 1);Recursive call moving right
int need = min(down, right) - dungeon[i][j];Calculate health needed at current cell
var calculateMinimumHP = function(dungeon) {
const m = dungeon.length, n = dungeon[0].length;
const memo = new Map();
function key(i, j) { return i + ',' + j; }
function dfs(i, j) {
if (i === m || j === n) return Infinity;
if (i === m - 1 && j === n - 1) return Math.max(1, 1 - dungeon[i][j]);
const k = key(i, j);
if (memo.has(k)) return memo.get(k);
const down = dfs(i + 1, j);
const right = dfs(i, j + 1);
const need = Math.min(down, right) - dungeon[i][j];
const res = Math.max(1, need);
memo.set(k, res);
return res;
}
return dfs(0, 0);
};
// Example usage:
// console.log(calculateMinimumHP([[-2,-3,3],[-5,-10,1],[10,30,-5]])); // Output: 7
Line Notes
const memo = new Map();Initialize memo map to cache results
function key(i, j) { return i + ',' + j; }Create unique key for memo lookup
if (memo.has(k)) return memo.get(k);Return cached result if available
memo.set(k, res);Store computed result in memo
function dfs(i, j) {Recursive helper to compute minimum health from cell (i,j)
if (i === m || j === n) return Infinity;Out-of-bounds returns Infinity to ignore invalid paths
if (i === m - 1 && j === n - 1) return Math.max(1, 1 - dungeon[i][j]);Base case at princess cell
const down = dfs(i + 1, j);Recursive call moving down
const right = dfs(i, j + 1);Recursive call moving right
const need = Math.min(down, right) - dungeon[i][j];Calculate health needed at current cell
TimeO(m*n)
SpaceO(m*n) for memo and recursion stack
Each cell is computed once and stored, avoiding repeated work.
💡 For a 200x200 grid, this means 40,000 computations, which is efficient.
Interview Verdict: Accepted
Memoization makes the solution efficient enough for large inputs while keeping recursion intuitive.