Top-down memoization
Top-down dynamic programming: recursive memoization, sentinel values for 'not computed', caching impossible states, recursion depth and the saving over pure recursion.
Learning goals
- Add a cache to a recursive function without changing its logic
- Choose a sentinel that cannot be confused with a real answer
- Cache 'impossible' results so failing branches are not repeated
- Recognise the recursion-depth limit and work around it
- Measure the saving over pure recursion
Prerequisites
Top-down dynamic programming keeps the recursion you already wrote and adds a memo table. It is often the fastest route from a recurrence to a working program.
Recursive memo
**Memoization** wraps a recursive function with a lookup: before computing a state, check whether its answer is already stored; after computing it, store it. The function's logic is unchanged, and each distinct state is computed once.
The memoised Fibonacci animation on n = 5 makes 9 calls instead of 15, and on n = 10 it makes 19 calls instead of 177. The extra calls are the cache hits: they return at once. The recursion tree shrinks from a bush to nearly a single path with hanging leaves.
1static long[] memo;23static long fibMemo(int n) {4if (n <= 1) return n;5if (memo[n] != 0) return memo[n]; // already solved6return memo[n] = fibMemo(n - 1) + fibMemo(n - 2);7}89static long fibonacci(int n) {10memo = new long[Math.max(n + 1, 2)];11return fibMemo(n);12}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(n) for the memo and the recursion
Fibonacci with memoization
Interactive · edit the inputRecursion tree · 1 call
Call fib(6).
1static long[] memo = new long[100];2static long fib(int n) {3if (n <= 1) return n; // base cases4if (memo[n] != 0) return memo[n]; // already solved: reuse it5return memo[n] = fib(n - 1) + fib(n - 2); // solve once, remember6}
Live state
n
6
depth
1
Call stack (top first)
- fib(6)
Execution trace (dry run table)Show 52 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 depth=1 | Call fib(6). |
| 2 | L3 | n=6 | Not a base case. |
| 3 | L4 | n=6 memo[n]=0 | memo[6] is empty, so compute it. |
| 4 | L5 | n=6 | fib(6) = fib(5) + fib(4). Call fib(5) first. |
| 5 | L2 | n=5 depth=2 | Call fib(5). |
| 6 | L3 | n=5 | Not a base case. |
| 7 | L4 | n=5 memo[n]=0 | memo[5] is empty, so compute it. |
| 8 | L5 | n=5 | fib(5) = fib(4) + fib(3). Call fib(4) first. |
| 9 | L2 | n=4 depth=3 | Call fib(4). |
| 10 | L3 | n=4 | Not a base case. |
| 11 | L4 | n=4 memo[n]=0 | memo[4] is empty, so compute it. |
| 12 | L5 | n=4 | fib(4) = fib(3) + fib(2). Call fib(3) first. |
| 13 | L2 | n=3 depth=4 | Call fib(3). |
| 14 | L3 | n=3 | Not a base case. |
| 15 | L4 | n=3 memo[n]=0 | memo[3] is empty, so compute it. |
| 16 | L5 | n=3 | fib(3) = fib(2) + fib(1). Call fib(2) first. |
| 17 | L2 | n=2 depth=5 | Call fib(2). |
| 18 | L3 | n=2 | Not a base case. |
| 19 | L4 | n=2 memo[n]=0 | memo[2] is empty, so compute it. |
| 20 | L5 | n=2 | fib(2) = fib(1) + fib(0). Call fib(1) first. |
| 21 | L2 | n=1 depth=6 | Call fib(1). |
| 22 | L3 | n=1 | Base case: fib(1) = 1. |
| 23 | L3 | n=1 return=1 | return 1. |
| 24 | L5 | n=2 left=1 | fib(1) = 1. Now call fib(0). |
| 25 | L2 | n=0 depth=6 | Call fib(0). |
| 26 | L3 | n=0 | Base case: fib(0) = 0. |
| 27 | L3 | n=0 return=0 | return 0. |
| 28 | L5 | n=2 left=1 right=0 return=1 | fib(2) = 1 + 0 = 1. Store it in memo[2]. |
| 29 | L5 | n=3 left=1 | fib(2) = 1. Now call fib(1). |
| 30 | L2 | n=1 depth=5 | Call fib(1). |
| 31 | L3 | n=1 | Base case: fib(1) = 1. |
| 32 | L3 | n=1 return=1 | return 1. |
| 33 | L5 | n=3 left=1 right=1 return=2 | fib(3) = 1 + 1 = 2. Store it in memo[3]. |
| 34 | L5 | n=4 left=2 | fib(3) = 2. Now call fib(2). |
| 35 | L2 | n=2 depth=4 | Call fib(2). |
| 36 | L3 | n=2 | Not a base case. |
| 37 | L4 | n=2 memo[n]=1 | memo[2] is already 1. No need to recurse again. |
| 38 | L4 | n=2 return=1 | Cache hit: return 1 immediately. This whole subtree is skipped. |
| 39 | L5 | n=4 left=2 right=1 return=3 | fib(4) = 2 + 1 = 3. Store it in memo[4]. |
| 40 | L5 | n=5 left=3 | fib(4) = 3. Now call fib(3). |
| 41 | L2 | n=3 depth=3 | Call fib(3). |
| 42 | L3 | n=3 | Not a base case. |
| 43 | L4 | n=3 memo[n]=2 | memo[3] is already 2. No need to recurse again. |
| 44 | L4 | n=3 return=2 | Cache hit: return 2 immediately. This whole subtree is skipped. |
| 45 | L5 | n=5 left=3 right=2 return=5 | fib(5) = 3 + 2 = 5. Store it in memo[5]. |
| 46 | L5 | n=6 left=5 | fib(5) = 5. Now call fib(4). |
| 47 | L2 | n=4 depth=2 | Call fib(4). |
| 48 | L3 | n=4 | Not a base case. |
| 49 | L4 | n=4 memo[n]=3 | memo[4] is already 3. No need to recurse again. |
| 50 | L4 | n=4 return=3 | Cache hit: return 3 immediately. This whole subtree is skipped. |
| 51 | L5 | n=6 left=5 right=3 return=8 | fib(6) = 5 + 3 = 8. Store it in memo[6]. |
| 52 | L5 | return=8 | fib(6) = 8 with 11 calls. Plain recursion would need 25 calls. |
Sentinel values
A memo table needs a value meaning **not computed yet**, distinct from every real answer. Zero often fails because zero can be a real answer (the fewest coins for amount 0 is 0). Use a special value outside the answer range, such as -1 when answers are never negative, or a boxed Integer[] where null means not computed.
If the answer itself can be -1 (for example, 'impossible'), use a different sentinel such as -2 for 'not computed'. The animation counts both calls and cache hits: on the “Memoized” preset for coins 1, 3, 4 and amount 6 there are 14 calls and 5 cache hits.
1static final int UNSET = -2, IMPOSSIBLE = -1;23static int coinChangeTopDown(int[] coins, int amount) {4int[] memo = new int[amount + 1];5Arrays.fill(memo, UNSET); // -2 can never be a real answer6return solve(coins, amount, memo);7}89static int solve(int[] coins, int amount, int[] memo) {10if (amount == 0) return 0;11if (memo[amount] != UNSET) return memo[amount];12int best = Integer.MAX_VALUE;13for (int c : coins) {14if (c > amount) continue;15int sub = solve(coins, amount - c, memo);16if (sub != IMPOSSIBLE) best = Math.min(best, sub + 1);17}18return memo[amount] = best == Integer.MAX_VALUE ? IMPOSSIBLE : best;19}
Top-down coin change: plain recursion vs memoization
Interactive · edit the inputTop-down recursion with a cache: each amount is solved at most once.
1static int[] memo; // memo[a]: -2 = not computed yet, -1 = impossible, otherwise the fewest coins2static int f(int[] coins, int a, boolean useMemo) {3if (a == 0) return 0; // base case4if (useMemo && memo[a] != -2) return memo[a]; // cache hit: this state is already solved (even if it was impossible)5int best = -1;6for (int c : coins) {7if (c > a) continue;8int sub = f(coins, a - c, useMemo);9if (sub >= 0 && (best < 0 || sub + 1 < best)) best = sub + 1;10}11if (useMemo) memo[a] = best; // store the answer, including -112return best;13}
Live state
calls
0
cache hits
0
amount
6
Call stack (top first)
- empty
Execution trace (dry run table)Show 30 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | calls=0 cache hits=0 amount=6 | Top-down recursion with a cache: each amount is solved at most once. |
| 2 | L2 | calls=1 cache hits=0 a=6 | Call f(6): the fewest coins for amount 6. |
| 3 | L2 | calls=2 cache hits=0 a=5 | Call f(5): the fewest coins for amount 5. |
| 4 | L2 | calls=3 cache hits=0 a=4 | Call f(4): the fewest coins for amount 4. |
| 5 | L2 | calls=4 cache hits=0 a=3 | Call f(3): the fewest coins for amount 3. |
| 6 | L2 | calls=5 cache hits=0 a=2 | Call f(2): the fewest coins for amount 2. |
| 7 | L2 | calls=6 cache hits=0 a=1 | Call f(1): the fewest coins for amount 1. |
| 8 | L2 | calls=7 cache hits=0 a=0 | Call f(0): the fewest coins for amount 0. |
| 9 | L3 | calls=7 cache hits=0 a=0 | Amount 0 needs 0 coins. |
| 10 | L12 | calls=7 cache hits=0 a=1 | f(1) = 1. Stored in the memo. |
| 11 | L12 | calls=7 cache hits=0 a=2 | f(2) = 2. Stored in the memo. |
| 12 | L2 | calls=8 cache hits=0 a=0 | Call f(0): the fewest coins for amount 0. |
| 13 | L3 | calls=8 cache hits=0 a=0 | Amount 0 needs 0 coins. |
| 14 | L12 | calls=8 cache hits=0 a=3 | f(3) = 1. Stored in the memo. |
| 15 | L2 | calls=9 cache hits=0 a=1 | Call f(1): the fewest coins for amount 1. |
| 16 | L4 | calls=9 cache hits=1 a=1 | Cache hit: f(1) = 1 is already known, so nothing below it is recomputed. |
| 17 | L2 | calls=10 cache hits=1 a=0 | Call f(0): the fewest coins for amount 0. |
| 18 | L3 | calls=10 cache hits=1 a=0 | Amount 0 needs 0 coins. |
| 19 | L12 | calls=10 cache hits=1 a=4 | f(4) = 1. Stored in the memo. |
| 20 | L2 | calls=11 cache hits=1 a=2 | Call f(2): the fewest coins for amount 2. |
| 21 | L4 | calls=11 cache hits=2 a=2 | Cache hit: f(2) = 2 is already known, so nothing below it is recomputed. |
| 22 | L2 | calls=12 cache hits=2 a=1 | Call f(1): the fewest coins for amount 1. |
| 23 | L4 | calls=12 cache hits=3 a=1 | Cache hit: f(1) = 1 is already known, so nothing below it is recomputed. |
| 24 | L12 | calls=12 cache hits=3 a=5 | f(5) = 2. Stored in the memo. |
| 25 | L2 | calls=13 cache hits=3 a=3 | Call f(3): the fewest coins for amount 3. |
| 26 | L4 | calls=13 cache hits=4 a=3 | Cache hit: f(3) = 1 is already known, so nothing below it is recomputed. |
| 27 | L2 | calls=14 cache hits=4 a=2 | Call f(2): the fewest coins for amount 2. |
| 28 | L4 | calls=14 cache hits=5 a=2 | Cache hit: f(2) = 2 is already known, so nothing below it is recomputed. |
| 29 | L12 | calls=14 cache hits=5 a=6 | f(6) = 2. Stored in the memo. |
| 30 | L12 | return=2 calls=14 cache hits=5 | Fewest coins: 2 after 14 calls (5 cache hits). There are only 6 distinct sub-problems, so the memoized version makes at most about 18 calls. |
Cache invalid states
A state whose answer is 'impossible' or 'no solution' is still an answer, and it must be cached like any other, or the same failing subtree is explored again every time it is reached. In the code above, memo[amount] = IMPOSSIBLE does that. Leaving it out is correct but can make a memoised solution as slow as plain recursion on inputs with many dead ends.
The “Impossible states are cached too” preset (coins 4 and 6, amount 9) shows the stored -1 results: no way to make 9 from 4 and 6 exists, and the animation finishes after 4 calls.
Top-down coin change: plain recursion vs memoization
Interactive · edit the inputTop-down recursion with a cache: each amount is solved at most once.
1static int[] memo; // memo[a]: -2 = not computed yet, -1 = impossible, otherwise the fewest coins2static int f(int[] coins, int a, boolean useMemo) {3if (a == 0) return 0; // base case4if (useMemo && memo[a] != -2) return memo[a]; // cache hit: this state is already solved (even if it was impossible)5int best = -1;6for (int c : coins) {7if (c > a) continue;8int sub = f(coins, a - c, useMemo);9if (sub >= 0 && (best < 0 || sub + 1 < best)) best = sub + 1;10}11if (useMemo) memo[a] = best; // store the answer, including -112return best;13}
Live state
calls
0
cache hits
0
amount
6
Call stack (top first)
- empty
Execution trace (dry run table)Show 30 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | calls=0 cache hits=0 amount=6 | Top-down recursion with a cache: each amount is solved at most once. |
| 2 | L2 | calls=1 cache hits=0 a=6 | Call f(6): the fewest coins for amount 6. |
| 3 | L2 | calls=2 cache hits=0 a=5 | Call f(5): the fewest coins for amount 5. |
| 4 | L2 | calls=3 cache hits=0 a=4 | Call f(4): the fewest coins for amount 4. |
| 5 | L2 | calls=4 cache hits=0 a=3 | Call f(3): the fewest coins for amount 3. |
| 6 | L2 | calls=5 cache hits=0 a=2 | Call f(2): the fewest coins for amount 2. |
| 7 | L2 | calls=6 cache hits=0 a=1 | Call f(1): the fewest coins for amount 1. |
| 8 | L2 | calls=7 cache hits=0 a=0 | Call f(0): the fewest coins for amount 0. |
| 9 | L3 | calls=7 cache hits=0 a=0 | Amount 0 needs 0 coins. |
| 10 | L12 | calls=7 cache hits=0 a=1 | f(1) = 1. Stored in the memo. |
| 11 | L12 | calls=7 cache hits=0 a=2 | f(2) = 2. Stored in the memo. |
| 12 | L2 | calls=8 cache hits=0 a=0 | Call f(0): the fewest coins for amount 0. |
| 13 | L3 | calls=8 cache hits=0 a=0 | Amount 0 needs 0 coins. |
| 14 | L12 | calls=8 cache hits=0 a=3 | f(3) = 1. Stored in the memo. |
| 15 | L2 | calls=9 cache hits=0 a=1 | Call f(1): the fewest coins for amount 1. |
| 16 | L4 | calls=9 cache hits=1 a=1 | Cache hit: f(1) = 1 is already known, so nothing below it is recomputed. |
| 17 | L2 | calls=10 cache hits=1 a=0 | Call f(0): the fewest coins for amount 0. |
| 18 | L3 | calls=10 cache hits=1 a=0 | Amount 0 needs 0 coins. |
| 19 | L12 | calls=10 cache hits=1 a=4 | f(4) = 1. Stored in the memo. |
| 20 | L2 | calls=11 cache hits=1 a=2 | Call f(2): the fewest coins for amount 2. |
| 21 | L4 | calls=11 cache hits=2 a=2 | Cache hit: f(2) = 2 is already known, so nothing below it is recomputed. |
| 22 | L2 | calls=12 cache hits=2 a=1 | Call f(1): the fewest coins for amount 1. |
| 23 | L4 | calls=12 cache hits=3 a=1 | Cache hit: f(1) = 1 is already known, so nothing below it is recomputed. |
| 24 | L12 | calls=12 cache hits=3 a=5 | f(5) = 2. Stored in the memo. |
| 25 | L2 | calls=13 cache hits=3 a=3 | Call f(3): the fewest coins for amount 3. |
| 26 | L4 | calls=13 cache hits=4 a=3 | Cache hit: f(3) = 1 is already known, so nothing below it is recomputed. |
| 27 | L2 | calls=14 cache hits=4 a=2 | Call f(2): the fewest coins for amount 2. |
| 28 | L4 | calls=14 cache hits=5 a=2 | Cache hit: f(2) = 2 is already known, so nothing below it is recomputed. |
| 29 | L12 | calls=14 cache hits=5 a=6 | f(6) = 2. Stored in the memo. |
| 30 | L12 | return=2 calls=14 cache hits=5 | Fewest coins: 2 after 14 calls (5 cache hits). There are only 6 distinct sub-problems, so the memoized version makes at most about 18 calls. |
Stack depth
Top-down recursion needs one stack frame per level of the longest dependency chain: the chain has length n for Fibonacci and the amount divided by the smallest coin for coin change. When that depth is large, Java throws StackOverflowError long before running out of heap. The frames are held on the thread stack, whose default size is small (a few hundred kilobytes to a megabyte, depending on the platform).
Three remedies: convert the memoised recursion to bottom-up tabulation (no recursion, no depth limit), run the recursion in a thread started with a larger stack size, or reduce the depth by processing states in an order that the recursion reaches shallowly. Tabulation is the robust answer, which is why the next lesson group is bottom-up.
1// run a deep recursion on a thread with a 256 MB stack2static long runDeep(int n) throws InterruptedException {3long[] result = new long[1];4Thread t = new Thread(null, () -> result[0] = depthSum(n), "deep", 256L << 20);5t.start();6t.join();7return result[0];8}910static long depthSum(int n) {11return n == 0 ? 0 : n + depthSum(n - 1); // recursion depth n12}
Fibonacci with memoization
Interactive · edit the inputRecursion tree · 1 call
Call fib(6).
1static long[] memo = new long[100];2static long fib(int n) {3if (n <= 1) return n; // base cases4if (memo[n] != 0) return memo[n]; // already solved: reuse it5return memo[n] = fib(n - 1) + fib(n - 2); // solve once, remember6}
Live state
n
6
depth
1
Call stack (top first)
- fib(6)
Execution trace (dry run table)Show 52 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 depth=1 | Call fib(6). |
| 2 | L3 | n=6 | Not a base case. |
| 3 | L4 | n=6 memo[n]=0 | memo[6] is empty, so compute it. |
| 4 | L5 | n=6 | fib(6) = fib(5) + fib(4). Call fib(5) first. |
| 5 | L2 | n=5 depth=2 | Call fib(5). |
| 6 | L3 | n=5 | Not a base case. |
| 7 | L4 | n=5 memo[n]=0 | memo[5] is empty, so compute it. |
| 8 | L5 | n=5 | fib(5) = fib(4) + fib(3). Call fib(4) first. |
| 9 | L2 | n=4 depth=3 | Call fib(4). |
| 10 | L3 | n=4 | Not a base case. |
| 11 | L4 | n=4 memo[n]=0 | memo[4] is empty, so compute it. |
| 12 | L5 | n=4 | fib(4) = fib(3) + fib(2). Call fib(3) first. |
| 13 | L2 | n=3 depth=4 | Call fib(3). |
| 14 | L3 | n=3 | Not a base case. |
| 15 | L4 | n=3 memo[n]=0 | memo[3] is empty, so compute it. |
| 16 | L5 | n=3 | fib(3) = fib(2) + fib(1). Call fib(2) first. |
| 17 | L2 | n=2 depth=5 | Call fib(2). |
| 18 | L3 | n=2 | Not a base case. |
| 19 | L4 | n=2 memo[n]=0 | memo[2] is empty, so compute it. |
| 20 | L5 | n=2 | fib(2) = fib(1) + fib(0). Call fib(1) first. |
| 21 | L2 | n=1 depth=6 | Call fib(1). |
| 22 | L3 | n=1 | Base case: fib(1) = 1. |
| 23 | L3 | n=1 return=1 | return 1. |
| 24 | L5 | n=2 left=1 | fib(1) = 1. Now call fib(0). |
| 25 | L2 | n=0 depth=6 | Call fib(0). |
| 26 | L3 | n=0 | Base case: fib(0) = 0. |
| 27 | L3 | n=0 return=0 | return 0. |
| 28 | L5 | n=2 left=1 right=0 return=1 | fib(2) = 1 + 0 = 1. Store it in memo[2]. |
| 29 | L5 | n=3 left=1 | fib(2) = 1. Now call fib(1). |
| 30 | L2 | n=1 depth=5 | Call fib(1). |
| 31 | L3 | n=1 | Base case: fib(1) = 1. |
| 32 | L3 | n=1 return=1 | return 1. |
| 33 | L5 | n=3 left=1 right=1 return=2 | fib(3) = 1 + 1 = 2. Store it in memo[3]. |
| 34 | L5 | n=4 left=2 | fib(3) = 2. Now call fib(2). |
| 35 | L2 | n=2 depth=4 | Call fib(2). |
| 36 | L3 | n=2 | Not a base case. |
| 37 | L4 | n=2 memo[n]=1 | memo[2] is already 1. No need to recurse again. |
| 38 | L4 | n=2 return=1 | Cache hit: return 1 immediately. This whole subtree is skipped. |
| 39 | L5 | n=4 left=2 right=1 return=3 | fib(4) = 2 + 1 = 3. Store it in memo[4]. |
| 40 | L5 | n=5 left=3 | fib(4) = 3. Now call fib(3). |
| 41 | L2 | n=3 depth=3 | Call fib(3). |
| 42 | L3 | n=3 | Not a base case. |
| 43 | L4 | n=3 memo[n]=2 | memo[3] is already 2. No need to recurse again. |
| 44 | L4 | n=3 return=2 | Cache hit: return 2 immediately. This whole subtree is skipped. |
| 45 | L5 | n=5 left=3 right=2 return=5 | fib(5) = 3 + 2 = 5. Store it in memo[5]. |
| 46 | L5 | n=6 left=5 | fib(5) = 5. Now call fib(4). |
| 47 | L2 | n=4 depth=2 | Call fib(4). |
| 48 | L3 | n=4 | Not a base case. |
| 49 | L4 | n=4 memo[n]=3 | memo[4] is already 3. No need to recurse again. |
| 50 | L4 | n=4 return=3 | Cache hit: return 3 immediately. This whole subtree is skipped. |
| 51 | L5 | n=6 left=5 right=3 return=8 | fib(6) = 5 + 3 = 8. Store it in memo[6]. |
| 52 | L5 | return=8 | fib(6) = 8 with 11 calls. Plain recursion would need 25 calls. |
Memoization vs pure recursion
Both versions compute the same function, but memoization turns an exponential number of calls into a polynomial one. The number of calls without memoization for Fibonacci is 2·F(n + 1) - 1, and with memoization it is 2n - 1.
The coin-change animation shows it on small input: for coins 1, 3, 4 and amount 6 the plain recursion makes 24 calls, and the memoised version makes 14 calls with 5 cache hits. There are only 6 distinct amounts to solve, so the memoised version can never solve more than 6 non-trivial states; the rest of its calls are cheap lookups.
| n | 5 | 6 | 8 | 10 |
|---|---|---|---|---|
| Fibonacci calls, plain recursion | 15 | 25 | 67 | 177 |
| Fibonacci calls, memoised | 9 | 11 | 15 | 19 |
Top-down coin change: plain recursion vs memoization
Interactive · edit the inputTop-down recursion with a cache: each amount is solved at most once.
1static int[] memo; // memo[a]: -2 = not computed yet, -1 = impossible, otherwise the fewest coins2static int f(int[] coins, int a, boolean useMemo) {3if (a == 0) return 0; // base case4if (useMemo && memo[a] != -2) return memo[a]; // cache hit: this state is already solved (even if it was impossible)5int best = -1;6for (int c : coins) {7if (c > a) continue;8int sub = f(coins, a - c, useMemo);9if (sub >= 0 && (best < 0 || sub + 1 < best)) best = sub + 1;10}11if (useMemo) memo[a] = best; // store the answer, including -112return best;13}
Live state
calls
0
cache hits
0
amount
6
Call stack (top first)
- empty
Execution trace (dry run table)Show 30 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | calls=0 cache hits=0 amount=6 | Top-down recursion with a cache: each amount is solved at most once. |
| 2 | L2 | calls=1 cache hits=0 a=6 | Call f(6): the fewest coins for amount 6. |
| 3 | L2 | calls=2 cache hits=0 a=5 | Call f(5): the fewest coins for amount 5. |
| 4 | L2 | calls=3 cache hits=0 a=4 | Call f(4): the fewest coins for amount 4. |
| 5 | L2 | calls=4 cache hits=0 a=3 | Call f(3): the fewest coins for amount 3. |
| 6 | L2 | calls=5 cache hits=0 a=2 | Call f(2): the fewest coins for amount 2. |
| 7 | L2 | calls=6 cache hits=0 a=1 | Call f(1): the fewest coins for amount 1. |
| 8 | L2 | calls=7 cache hits=0 a=0 | Call f(0): the fewest coins for amount 0. |
| 9 | L3 | calls=7 cache hits=0 a=0 | Amount 0 needs 0 coins. |
| 10 | L12 | calls=7 cache hits=0 a=1 | f(1) = 1. Stored in the memo. |
| 11 | L12 | calls=7 cache hits=0 a=2 | f(2) = 2. Stored in the memo. |
| 12 | L2 | calls=8 cache hits=0 a=0 | Call f(0): the fewest coins for amount 0. |
| 13 | L3 | calls=8 cache hits=0 a=0 | Amount 0 needs 0 coins. |
| 14 | L12 | calls=8 cache hits=0 a=3 | f(3) = 1. Stored in the memo. |
| 15 | L2 | calls=9 cache hits=0 a=1 | Call f(1): the fewest coins for amount 1. |
| 16 | L4 | calls=9 cache hits=1 a=1 | Cache hit: f(1) = 1 is already known, so nothing below it is recomputed. |
| 17 | L2 | calls=10 cache hits=1 a=0 | Call f(0): the fewest coins for amount 0. |
| 18 | L3 | calls=10 cache hits=1 a=0 | Amount 0 needs 0 coins. |
| 19 | L12 | calls=10 cache hits=1 a=4 | f(4) = 1. Stored in the memo. |
| 20 | L2 | calls=11 cache hits=1 a=2 | Call f(2): the fewest coins for amount 2. |
| 21 | L4 | calls=11 cache hits=2 a=2 | Cache hit: f(2) = 2 is already known, so nothing below it is recomputed. |
| 22 | L2 | calls=12 cache hits=2 a=1 | Call f(1): the fewest coins for amount 1. |
| 23 | L4 | calls=12 cache hits=3 a=1 | Cache hit: f(1) = 1 is already known, so nothing below it is recomputed. |
| 24 | L12 | calls=12 cache hits=3 a=5 | f(5) = 2. Stored in the memo. |
| 25 | L2 | calls=13 cache hits=3 a=3 | Call f(3): the fewest coins for amount 3. |
| 26 | L4 | calls=13 cache hits=4 a=3 | Cache hit: f(3) = 1 is already known, so nothing below it is recomputed. |
| 27 | L2 | calls=14 cache hits=4 a=2 | Call f(2): the fewest coins for amount 2. |
| 28 | L4 | calls=14 cache hits=5 a=2 | Cache hit: f(2) = 2 is already known, so nothing below it is recomputed. |
| 29 | L12 | calls=14 cache hits=5 a=6 | f(6) = 2. Stored in the memo. |
| 30 | L12 | return=2 calls=14 cache hits=5 | Fewest coins: 2 after 14 calls (5 cache hits). There are only 6 distinct sub-problems, so the memoized version makes at most about 18 calls. |
Practice ladder
Now try it yourself
Easy
- Fibonacci Number
- N-th Tribonacci Number
Medium
- Coin Change
- Climbing Stairs (top-down)
- Word Break
Advanced
- Target Sum
- Palindrome Partitioning II
Key takeaways
- Memoization adds a lookup before the computation and a store after it; the recurrence is unchanged.
- Use a sentinel that cannot be a real answer, and cache impossible states too.
- Recursion depth equals the longest dependency chain; deep chains need a bigger stack or tabulation.
- Memoised calls grow linearly where plain recursion grows exponentially.