DSA Dynamic Programming
Memoization
The UI freezes on every keystroke while a recursive scorer recomputes the same prefixes. Dual conflict: the recurrence is clear; the call tree is not.
Memoization is DP: check a , on a compute once, store, return. State design beats decorator tricks.
Algorithm journey
Include every result-affecting field.
Lookup first
Hit returns immediately; miss computes.
Store then return
Never confuse uncomputed with a real answer.
Long chains still need a stack-safe plan.
What actually happens
Naive idea: write the recursive formula and ship it. Correct for tiny inputs. Fibonacci-style trees recompute shared states exponentially.
Better idea: before computing a state, look it up. On a miss, recurse, store under a sound , return. Work drops to reachable states × transitions.
The improved design still fails with a wrong key, an ambiguous , cyclic dependencies without a decreasing measure, or stack overflow on a deep chain of unique states.
Invariant
If the memo maps key K to value V, then V is the correct answer for that exact subproblem. Uncomputed keys must not be confused with legitimate answers.
How to choose memoization
1. Is the recursive form natural? Prefer top-down when the recurrence matches how you think about the problem.
2. Sparse reachability? Memoization skips unreachable states that a full table would fill.
3. Stack or lifetime risk? Deep chains, shared process caches, or hot loops often favor tabulation or bounded caches.
Quantitative check
Naive Fib(40) is tens of millions of calls; memoized Fib(40) is 40 misses plus hits. At n = 10⁵ with a linear chain of unique states, time is fine but default recursion depth is not: convert to iteration.
Fibonacci Recursion Tree: fib(5)
Step 0 of -1
Cache
State design: the cache key is the algorithm
Sound key
Two calls may share an entry only if their remaining subproblems are equivalent. Include every input that can change the result: index, remaining budget, previous choice, flags, graph/version, and omit values derivable from those fields.
Stable representation
Use immutable tuples/records, canonicalize unordered data, and never mutate an object after using it as a hash key. If a dense integer state has known bounds, an array is faster and more memory-efficient than hashing.
Misses, cycles, and recursion depth
| Risk | Failure | Robust design |
|---|---|---|
| Ambiguous sentinel | 0, −1, or null can be a legitimate answer | Use membership checks, a separate visited array, Optional, or a private sentinel object |
| Cyclic state graph | A state recursively requests itself before completion | Prove a decreasing measure; otherwise use three colors (unseen, visiting, done) and solve the cycle with graph methods |
| Deep chain | O(states) work still overflows the call stack | Convert to tabulation/explicit stack; Python's recursion limit and Java's stack are not complexity guarantees |
| Cached exception | A transient failure becomes a persistent answer | Decide explicitly whether failures are cached and for how long |
Complexity: time is reachable states × transitions per state, plus key construction/hash cost. Space is cached states + recursion depth. Memoization does not automatically make an exponential state space polynomial.
Cache lifetime: DP memo versus service cache
Call-local
Create the memo per top-level problem. No invalidation or tenant leakage; the default for interview DP.
Object/request scoped
Reuse within a stable input version. Clear when configuration, graph, model, permissions, or locale changes.
Process/shared
Needs bounded size, eviction, versioned keys, observability, and an invalidation contract. TTL limits staleness; it does not prove correctness.
Python and Java production semantics
Python
- • functools.cache is unbounded; lru_cache(maxsize=…) bounds entries.
- • Arguments must be hashable, and equality/hash must reflect every result-affecting input.
- • Internal locking keeps the cache structure coherent, but two threads may still compute the same miss before either stores it.
Java
- • Prefer arrays + a visited bitmap for dense primitive states; use immutable record keys for maps.
- • HashMap is not concurrent. ConcurrentHashMap protects structure, not arbitrary recursive computation.
- • Be cautious with recursive computeIfAbsent; use an explicit lookup/compute/store pattern or single-flight abstraction.
Cache poisoning / cardinality attacks: untrusted keys can fill an unbounded cache, and missing tenant/auth/version fields can leak or reuse the wrong answer. Bound entries, validate key space, isolate security contexts, and monitor hit rate, size, evictions, and compute latency.
🔑 How Memoization Works
Check the Cache First
Before computing anything, check if you've already solved this exact subproblem. If yes, return the cached result immediately!
Compute If Not Cached
If this is the first time seeing this subproblem, solve it recursively (which may trigger more memoized calls for smaller subproblems).
Store the Result
Before returning, save the computed result in your cache (usually a dictionary or array). Future calls with the same input will use this cached value.
Return the Answer
Return the result (whether from cache or freshly computed). The caller doesn't know or care if it was cached!
🔍 Did You Know? Etymology of "Memoization"
The term "memoization" was coined by Donald Michie in 1968. It comes from the Latin "memorandum" (to be remembered), not "memorization"! The missing 'r' is intentional - it refers to storing a memo (note) of the result. Fun fact: Michie was a codebreaker at Bletchley Park during WWII, working alongside Alan Turing!
💻 Classic Example: Fibonacci with Memoization
❌ Without Memoization (Exponential Time)
Python
def fib_slow(n):
"""
Naive recursion - EXTREMELY SLOW!
Time: O(2^n) - exponential growth
Space: O(n) - recursion stack depth
Problem: Massive redundant calculations
fib(5) = fib(4) + fib(3)
= (fib(3) + fib(2)) + (fib(2) + fib(1))
= ((fib(2) + fib(1)) + fib(2)) + (fib(2) + fib(1))
fib(2) is calculated 3 times!
fib(3) is calculated 2 times!
For fib(40), there are 331,160,281 function calls!
"""
if n < 0:
raise ValueError("n must be non-negative")
if n <= 1:
return n
return fib_slow(n - 1) + fib_slow(n - 2)
# Try this - it takes several seconds!
import time
start = time.time()
result = fib_slow(35)
print(f"fib(35) = {result}, took {time.time() - start:.2f}s")
# Output: fib(35) = 9227465, took ~3.5sJava
import java.math.BigInteger;
public class FibonacciSlow {
/**
* Naive recursion - EXTREMELY SLOW!
* Time: O(2^n) - exponential growth
* Space: O(n) - recursion stack depth
*
* Problem: Massive redundant calculations
* fib(5) = fib(4) + fib(3)
* = (fib(3) + fib(2)) + (fib(2) + fib(1))
* = ((fib(2) + fib(1)) + fib(2)) + (fib(2) + fib(1))
*
* fib(2) is calculated 3 times!
* fib(3) is calculated 2 times!
* For fib(40), there are 331,160,281 function calls!
*/
public static BigInteger fibSlow(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
if (n <= 1) {
return BigInteger.valueOf(n);
}
return fibSlow(n - 1).add(fibSlow(n - 2));
}
public static void main(String[] args) {
// Try this - it takes several seconds!
long start = System.currentTimeMillis();
BigInteger result = fibSlow(35);
double elapsed = (System.currentTimeMillis() - start) / 1000.0;
System.out.printf("fib(35) = %d, took %.2fs%n", result, elapsed);
// Output: fib(35) = 9227465, took ~3.5s
}
}Recursion Tree Explosion: fib(5) makes 15 calls, fib(10) makes 177 calls, fib(20) makes 21,891 calls, fib(40) makes 331 MILLION calls! 🤯
✅ With Memoization (Linear Time)
Python
def fib_memo(n, memo=None):
"""
Memoization - LIGHTNING FAST!
Time: O(n) - each number computed once
Space: O(n) - memo dict + recursion stack
Each fib(k) is computed exactly once and cached
Subsequent calls return cached value in O(1)
"""
if n < 0:
raise ValueError("n must be non-negative")
# Initialize memo on first call
if memo is None:
memo = {}
# Step 1: Check cache first
if n in memo:
return memo[n] # Cache hit - instant return!
# Step 2: Cache base cases too.
if n <= 1:
memo[n] = n
return n
# Step 3: Compute recursively (may use cached values)
result = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
# Step 4: Store in cache before returning
memo[n] = result
return result
# Same calculation, but instant!
import time
start = time.time()
result = fib_memo(35)
print(f"fib(35) = {result}, took {time.time() - start:.6f}s")
# Output: fib(35) = 9227465, took 0.000050s
# Can even handle huge inputs!
print(f"fib(100) = {fib_memo(100)}")
# Output: fib(100) = 354224848179261915075Java
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
public class FibonacciMemo {
/**
* Memoization - LIGHTNING FAST!
* Time: O(n) - each number computed once
* Space: O(n) - memo map + recursion stack
*
* Each fib(k) is computed exactly once and cached
* Subsequent calls return cached value in O(1)
*/
public static BigInteger fibMemo(int n, Map<Integer, BigInteger> memo) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
// Step 1: Check cache first
if (memo.containsKey(n)) {
return memo.get(n); // Cache hit - instant return!
}
// Step 2: Cache base cases too.
if (n <= 1) {
BigInteger base = BigInteger.valueOf(n);
memo.put(n, base);
return base;
}
// Step 3: Compute recursively (may use cached values)
BigInteger result = fibMemo(n - 1, memo).add(fibMemo(n - 2, memo));
// Step 4: Store in cache before returning
memo.put(n, result);
return result;
}
public static void main(String[] args) {
Map<Integer, BigInteger> memo = new HashMap<>();
// Same calculation, but instant!
long start = System.nanoTime();
BigInteger result = fibMemo(35, memo);
double elapsed = (System.nanoTime() - start) / 1_000_000_000.0;
System.out.printf("fib(35) = %d, took %.6fs%n", result, elapsed);
// Output: fib(35) = 9227465, took 0.000050s
// BigInteger preserves exact values beyond signed long.
memo.clear();
BigInteger expected = new BigInteger("354224848179261915075");
if (!fibMemo(100, memo).equals(expected)) throw new AssertionError();
}
}Dramatic Speedup: fib(35) goes from 3.5 seconds to 0.00005 seconds - that's 70,000x faster! All by remembering previous results. 🚀
🐍 Python's @lru_cache Decorator
Python
from functools import lru_cache
@lru_cache(maxsize=None) # Unlimited cache size
def fib_cached(n):
"""
Python's built-in memoization decorator!
Automatically caches function results
lru_cache = Least Recently Used cache
maxsize=None means unlimited cache
"""
if n <= 1:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
# Just as fast as manual memoization!
print(fib_cached(100))
# Output: 354224848179261915075
# Can check cache stats
print(fib_cached.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)
# 98 cache hits, 101 misses (first-time computations)Java
import java.util.HashMap;
import java.util.Map;
public class MemoizationWrapper {
// Use explicit lookup/compute/store for recursive memoization.
static long fib(int n, Map<Integer, Long> cache) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
Long cached = cache.get(n);
if (cached != null) return cached;
long result;
if (n <= 1) {
result = n;
} else {
result = Math.addExact(fib(n - 1, cache), fib(n - 2, cache));
}
cache.put(n, result);
return result;
}
public static void main(String[] args) {
Map<Integer, Long> cache = new HashMap<>();
if (fib(50, cache) != 12_586_269_025L) throw new AssertionError();
if (cache.size() != 51) throw new AssertionError();
}
}🎯 Key Insight: Top-Down Thinking
Memoization is top-down because you start with the original problem and break it down:
fib(5) → needs fib(4) and fib(3)
fib(4) → needs fib(3) and fib(2)
fib(3) → needs fib(2) and fib(1)
fib(2) → needs fib(1) and fib(0) ✓ base case
You naturally think recursively ("to solve this, I need..."), and memoization makes it efficient!
🎯 More Memoization Examples
Grid Paths (2D Memoization)
Python
def count_paths(m, n, memo=None):
"""
Count paths from (0,0) to (m-1, n-1)
Can only move right or down
Time: O(m*n) with memoization
Space: O(m*n) for memo
"""
if m <= 0 or n <= 0:
raise ValueError("grid dimensions must be positive")
if memo is None:
memo = {}
# Check cache (use tuple as key for 2D)
if (m, n) in memo:
return memo[(m, n)]
# Base cases
if m == 1 or n == 1:
return 1
# Recursive case: paths = paths from left + paths from top
memo[(m, n)] = count_paths(m-1, n, memo) + count_paths(m, n-1, memo)
return memo[(m, n)]
print(count_paths(3, 3)) # 6 paths
print(count_paths(10, 10)) # 48,620 paths - instant!Java
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
public class GridPaths {
record State(int rows, int columns) {}
/**
* Count paths from (0,0) to (m-1, n-1)
* Can only move right or down
*
* Time: O(m*n) with memoization
* Space: O(m*n) for memo
*/
public static BigInteger countPaths(int m, int n, Map<State, BigInteger> memo) {
if (m <= 0 || n <= 0) throw new IllegalArgumentException("dimensions must be positive");
State key = new State(m, n);
if (memo.containsKey(key)) {
return memo.get(key);
}
// Base cases
if (m == 1 || n == 1) {
return BigInteger.ONE;
}
// Recursive case: paths = paths from left + paths from top
BigInteger result = countPaths(m - 1, n, memo).add(countPaths(m, n - 1, memo));
memo.put(key, result);
return result;
}
public static void main(String[] args) {
Map<State, BigInteger> memo = new HashMap<>();
if (!countPaths(3, 3, memo).equals(BigInteger.valueOf(6))) throw new AssertionError();
memo.clear();
if (!countPaths(10, 10, memo).equals(BigInteger.valueOf(48_620))) throw new AssertionError();
}
}Longest Common Subsequence
Python
def lcs(s1, s2):
"""
Longest Common Subsequence with memoization
Time: O(m*n) with memoization
Without: O(2^(m+n)) - exponential!
"""
memo = {} # Call-local: entries can never leak across a different string pair.
def solve(i, j):
if i < 0 or j < 0:
return 0
if (i, j) in memo:
return memo[(i, j)]
if s1[i] == s2[j]:
result = 1 + solve(i - 1, j - 1)
else:
result = max(solve(i - 1, j), solve(i, j - 1))
memo[(i, j)] = result
return result
return solve(len(s1) - 1, len(s2) - 1)
print(lcs("ABCDGH", "AEDFHR")) # 3 (ADH)
print(lcs("AGGTAB", "GXTXAYB")) # 4 (GTAB)Java
import java.util.HashMap;
import java.util.Map;
public class LongestCommonSubsequence {
record State(int firstIndex, int secondIndex) {}
/**
* Longest Common Subsequence with memoization
*
* Time: O(m*n) with memoization
* Without: O(2^(m+n)) - exponential!
*/
private static int solve(int[] first, int[] second, int i, int j,
Map<State, Integer> memo) {
if (i < 0 || j < 0) {
return 0;
}
State state = new State(i, j);
Integer cached = memo.get(state);
if (cached != null) return cached;
int result;
if (first[i] == second[j]) {
result = 1 + solve(first, second, i - 1, j - 1, memo);
} else {
result = Math.max(solve(first, second, i - 1, j, memo),
solve(first, second, i, j - 1, memo));
}
memo.put(state, result);
return result;
}
public static int lcs(String s1, String s2) {
int[] first = s1.codePoints().toArray();
int[] second = s2.codePoints().toArray();
return solve(first, second, first.length - 1, second.length - 1, new HashMap<>());
}
public static void main(String[] args) {
System.out.println(lcs("ABCDGH", "AEDFHR")); // 3 (ADH)
System.out.println(lcs("AGGTAB", "GXTXAYB")); // 4 (GTAB)
if (lcs("🙂A", "🙂B") != 1) throw new AssertionError();
}
}💡 Pro Tip: When to Use Memoization
Choose memoization over tabulation when:
- • Recursive solution is natural: Problem is easier to think about recursively
- • Not all subproblems needed: Only solve what's actually required (lazy evaluation)
- • Complex state space: Multi-dimensional or irregular state representation
- • Quick prototyping: Faster to implement (just add caching to recursion)
Avoid when: Recursion depth is too large (stack overflow risk) or you need all subproblems anyway!
✅ Pros & ❌ Cons
✅ Advantages
- ✓Natural recursive thinking - easier to understand
- ✓Only computes needed subproblems (lazy evaluation)
- ✓Easy to implement - add caching to recursion
- ✓Works well with complex/irregular state spaces
- ✓Python's @lru_cache makes it trivial
❌ Disadvantages
- ✗Recursion overhead (function call stack)
- ✗Risk of stack overflow for deep recursion
- ✗Uses more space (cache + call stack)
- ✗Harder to optimize space than tabulation
- ✗May add recursion, hashing, and function-call overhead versus a dense table
💼 Real World Applications
🌐 Web Browsers
Browsers memoize DNS lookups, parsed CSS, compiled JavaScript, and rendered layouts. Revisiting a page is instant because results are cached!
Example: Browser cache, CDN caching
🎮 Game AI
Chess engines and game AI use memoization (transposition tables) to cache board evaluations. Same position reached via different moves? Use cached score!
Example: Chess engines, Go AI
⚛️ React Rendering
React's useMemo can reuse a calculation during a render, while useCallback can stabilize a function reference. They do not prevent a component render by themselves; a stable prop can help a separately memoized child skip work.
Example: React optimization, Vue computed properties
🔬 Scientific Computing
Scientific applications can add domain-specific caches for deterministic, repeatedly requested simulations, dynamic-programming states, or objective-function evaluations when the inputs and model version form a sound key.
Example: application-level caches in optimization and sequence analysis
Exercises
- Knapsack memo keyed only by index (not remaining capacity): construct a tiny input that returns the wrong optimum. What field was missing from the ?
- Fib(n) for n = 10⁵ with default Python recursion. Does memoization alone save you? What fails first: time or stack?
- When would you prefer an unbounded
@cacheversuslru_cache(maxsize=…)in a long-lived service?
Primary sources
- Michie, D. (1968): “Memo” functions and machine learning (origin of the term).
- Cormen et al., CLRS: top-down with memoization vs bottom-up.
- Python docs:
functools.lru_cache/cache(hashability and eviction).
Takeaway
Memoization is top-down DP with a correctness contract on the cache key. Hits save work; wrong keys save wrong answers.