🧠
Top-Down DP with Memoization
💡 Memoization avoids recomputing the same subproblems by caching results, drastically improving efficiency.
Intuition
Store results of recursive calls in a cache so that repeated calls return instantly instead of recomputing.
Algorithm
- Initialize a cache (2D array) to store minimum path sums for each position.
- Modify the recursive function to check the cache before computing.
- If cached, return the stored value; otherwise compute and store it.
- Return the cached result for the top position.
💡 Memoization transforms exponential recursion into polynomial time by eliminating duplicate work.
Recurrence:dp[i][j] = triangle[i][j] + min(dp[i+1][j], dp[i+1][j+1])
from typing import List
def minimumTotal(triangle: List[List[int]]) -> int:
n = len(triangle)
memo = [[None]*len(row) for row in triangle]
def dfs(i, j):
if i == n - 1:
return triangle[i][j]
if memo[i][j] is not None:
return memo[i][j]
left = dfs(i + 1, j)
right = dfs(i + 1, j + 1)
memo[i][j] = triangle[i][j] + min(left, right)
return memo[i][j]
return dfs(0, 0)
# Example usage
if __name__ == "__main__":
tri = [[2],[3,4],[6,5,7],[4,1,8,3]]
print(minimumTotal(tri)) # Output: 11
Line Notes
memo = [[None]*len(row) for row in triangle]Initialize cache with None to mark uncomputed states
if memo[i][j] is not None:Return cached result if available to avoid recomputation
memo[i][j] = triangle[i][j] + min(left, right)Store computed minimum path sum in cache
return memo[i][j]Return cached or newly computed result
import java.util.*;
public class Solution {
private Integer[][] memo;
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
memo = new Integer[n][];
for (int i = 0; i < n; i++) {
memo[i] = new Integer[triangle.get(i).size()];
}
return dfs(triangle, 0, 0);
}
private int dfs(List<List<Integer>> triangle, int i, int j) {
if (i == triangle.size() - 1) {
return triangle.get(i).get(j);
}
if (memo[i][j] != null) {
return memo[i][j];
}
int left = dfs(triangle, i + 1, j);
int right = dfs(triangle, i + 1, j + 1);
memo[i][j] = triangle.get(i).get(j) + Math.min(left, right);
return memo[i][j];
}
public static void main(String[] args) {
Solution sol = new Solution();
List<List<Integer>> tri = new ArrayList<>();
tri.add(Arrays.asList(2));
tri.add(Arrays.asList(3,4));
tri.add(Arrays.asList(6,5,7));
tri.add(Arrays.asList(4,1,8,3));
System.out.println(sol.minimumTotal(tri)); // 11
}
}
Line Notes
memo = new Integer[n][];Initialize memo array with nulls to mark uncomputed states
if (memo[i][j] != null)Return cached result if available
memo[i][j] = triangle.get(i).get(j) + Math.min(left, right);Store computed minimum path sum
return memo[i][j];Return cached or computed result
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution {
vector<vector<int>> memo;
public:
int dfs(const vector<vector<int>>& triangle, int i, int j) {
if (i == triangle.size() - 1) return triangle[i][j];
if (memo[i][j] != INT_MAX) return memo[i][j];
int left = dfs(triangle, i + 1, j);
int right = dfs(triangle, i + 1, j + 1);
memo[i][j] = triangle[i][j] + min(left, right);
return memo[i][j];
}
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
memo.assign(n, vector<int>(n, INT_MAX));
return dfs(triangle, 0, 0);
}
};
int main() {
Solution sol;
vector<vector<int>> tri = {{2},{3,4},{6,5,7},{4,1,8,3}};
cout << sol.minimumTotal(tri) << endl; // 11
return 0;
}
Line Notes
memo.assign(n, vector<int>(n, INT_MAX));Initialize memo with sentinel INT_MAX to mark uncomputed
if (memo[i][j] != INT_MAX) return memo[i][j];Return cached result if computed
memo[i][j] = triangle[i][j] + min(left, right);Store computed minimum path sum
return memo[i][j];Return cached or computed result
function minimumTotal(triangle) {
const n = triangle.length;
const memo = Array.from({length: n}, (v, i) => Array(triangle[i].length).fill(null));
function dfs(i, j) {
if (i === n - 1) return triangle[i][j];
if (memo[i][j] !== null) return memo[i][j];
const left = dfs(i + 1, j);
const right = dfs(i + 1, j + 1);
memo[i][j] = triangle[i][j] + Math.min(left, right);
return memo[i][j];
}
return dfs(0, 0);
}
// Example usage
const tri = [[2],[3,4],[6,5,7],[4,1,8,3]];
console.log(minimumTotal(tri)); // 11
Line Notes
const memo = Array.from({length: n}, (v, i) => Array(triangle[i].length).fill(null));Initialize memo array with nulls
if (memo[i][j] !== null) return memo[i][j];Return cached result if available
memo[i][j] = triangle[i][j] + Math.min(left, right);Store computed minimum path sum
return memo[i][j];Return cached or computed result
TimeO(n^2)
SpaceO(n^2) for memo and recursion stack
Each subproblem is computed once and stored, reducing exponential calls to polynomial.
💡 For n=200 rows, this means about 40,000 computations, which is efficient enough.
Interview Verdict: Accepted
Memoization makes the solution efficient and acceptable for interviews.