Chapter 23: Dynamic Programming: The Core Method
23.03Intermediate5 topics15 min5 interactive animations

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

The DP workflowRecursion

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.

23.03.1

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.

Java
1static long[] memo;
2
3static long fibMemo(int n) {
4 if (n <= 1) return n;
5 if (memo[n] != 0) return memo[n]; // already solved
6 return memo[n] = fibMemo(n - 1) + fibMemo(n - 2);
7}
8
9static long fibonacci(int n) {
10 memo = new long[Math.max(n + 1, 2)];
11 return 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 input

Your input

Try

Recursion tree · 1 call

fib(6)…
Running now
Call

Call fib(6).

Step 1 / 52
Java
1static long[] memo = new long[100];
2static long fib(int n) {
3 if (n <= 1) return n; // base cases
4 if (memo[n] != 0) return memo[n]; // already solved: reuse it
5 return memo[n] = fib(n - 1) + fib(n - 2); // solve once, remember
6}

Live state

1 calls

n

6

depth

1

No condition evaluated on this step

Call stack (top first)

  1. fib(6)
Execution trace (dry run table)Show 52 steps
StepLineStateWhat happens
1L2n=6 depth=1Call fib(6).
2L3n=6Not a base case.
3L4n=6 memo[n]=0memo[6] is empty, so compute it.
4L5n=6fib(6) = fib(5) + fib(4). Call fib(5) first.
5L2n=5 depth=2Call fib(5).
6L3n=5Not a base case.
7L4n=5 memo[n]=0memo[5] is empty, so compute it.
8L5n=5fib(5) = fib(4) + fib(3). Call fib(4) first.
9L2n=4 depth=3Call fib(4).
10L3n=4Not a base case.
11L4n=4 memo[n]=0memo[4] is empty, so compute it.
12L5n=4fib(4) = fib(3) + fib(2). Call fib(3) first.
13L2n=3 depth=4Call fib(3).
14L3n=3Not a base case.
15L4n=3 memo[n]=0memo[3] is empty, so compute it.
16L5n=3fib(3) = fib(2) + fib(1). Call fib(2) first.
17L2n=2 depth=5Call fib(2).
18L3n=2Not a base case.
19L4n=2 memo[n]=0memo[2] is empty, so compute it.
20L5n=2fib(2) = fib(1) + fib(0). Call fib(1) first.
21L2n=1 depth=6Call fib(1).
22L3n=1Base case: fib(1) = 1.
23L3n=1 return=1return 1.
24L5n=2 left=1fib(1) = 1. Now call fib(0).
25L2n=0 depth=6Call fib(0).
26L3n=0Base case: fib(0) = 0.
27L3n=0 return=0return 0.
28L5n=2 left=1 right=0 return=1fib(2) = 1 + 0 = 1. Store it in memo[2].
29L5n=3 left=1fib(2) = 1. Now call fib(1).
30L2n=1 depth=5Call fib(1).
31L3n=1Base case: fib(1) = 1.
32L3n=1 return=1return 1.
33L5n=3 left=1 right=1 return=2fib(3) = 1 + 1 = 2. Store it in memo[3].
34L5n=4 left=2fib(3) = 2. Now call fib(2).
35L2n=2 depth=4Call fib(2).
36L3n=2Not a base case.
37L4n=2 memo[n]=1memo[2] is already 1. No need to recurse again.
38L4n=2 return=1Cache hit: return 1 immediately. This whole subtree is skipped.
39L5n=4 left=2 right=1 return=3fib(4) = 2 + 1 = 3. Store it in memo[4].
40L5n=5 left=3fib(4) = 3. Now call fib(3).
41L2n=3 depth=3Call fib(3).
42L3n=3Not a base case.
43L4n=3 memo[n]=2memo[3] is already 2. No need to recurse again.
44L4n=3 return=2Cache hit: return 2 immediately. This whole subtree is skipped.
45L5n=5 left=3 right=2 return=5fib(5) = 3 + 2 = 5. Store it in memo[5].
46L5n=6 left=5fib(5) = 5. Now call fib(4).
47L2n=4 depth=2Call fib(4).
48L3n=4Not a base case.
49L4n=4 memo[n]=3memo[4] is already 3. No need to recurse again.
50L4n=4 return=3Cache hit: return 3 immediately. This whole subtree is skipped.
51L5n=6 left=5 right=3 return=8fib(6) = 5 + 3 = 8. Store it in memo[6].
52L5return=8fib(6) = 8 with 11 calls. Plain recursion would need 25 calls.
23.03.2

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.

Java
1static final int UNSET = -2, IMPOSSIBLE = -1;
2
3static int coinChangeTopDown(int[] coins, int amount) {
4 int[] memo = new int[amount + 1];
5 Arrays.fill(memo, UNSET); // -2 can never be a real answer
6 return solve(coins, amount, memo);
7}
8
9static int solve(int[] coins, int amount, int[] memo) {
10 if (amount == 0) return 0;
11 if (memo[amount] != UNSET) return memo[amount];
12 int best = Integer.MAX_VALUE;
13 for (int c : coins) {
14 if (c > amount) continue;
15 int sub = solve(coins, amount - c, memo);
16 if (sub != IMPOSSIBLE) best = Math.min(best, sub + 1);
17 }
18 return memo[amount] = best == Integer.MAX_VALUE ? IMPOSSIBLE : best;
19}

Top-down coin change: plain recursion vs memoization

Interactive · edit the input

Your input

Try
Call

Top-down recursion with a cache: each amount is solved at most once.

Step 1 / 30
Java
1static int[] memo; // memo[a]: -2 = not computed yet, -1 = impossible, otherwise the fewest coins
2static int f(int[] coins, int a, boolean useMemo) {
3 if (a == 0) return 0; // base case
4 if (useMemo && memo[a] != -2) return memo[a]; // cache hit: this state is already solved (even if it was impossible)
5 int best = -1;
6 for (int c : coins) {
7 if (c > a) continue;
8 int sub = f(coins, a - c, useMemo);
9 if (sub >= 0 && (best < 0 || sub + 1 < best)) best = sub + 1;
10 }
11 if (useMemo) memo[a] = best; // store the answer, including -1
12 return best;
13}

Live state

0 calls0 cache hits

calls

0

cache hits

0

amount

6

No condition evaluated on this step

Call stack (top first)

  1. empty
Execution trace (dry run table)Show 30 steps
StepLineStateWhat happens
1L1calls=0 cache hits=0 amount=6Top-down recursion with a cache: each amount is solved at most once.
2L2calls=1 cache hits=0 a=6Call f(6): the fewest coins for amount 6.
3L2calls=2 cache hits=0 a=5Call f(5): the fewest coins for amount 5.
4L2calls=3 cache hits=0 a=4Call f(4): the fewest coins for amount 4.
5L2calls=4 cache hits=0 a=3Call f(3): the fewest coins for amount 3.
6L2calls=5 cache hits=0 a=2Call f(2): the fewest coins for amount 2.
7L2calls=6 cache hits=0 a=1Call f(1): the fewest coins for amount 1.
8L2calls=7 cache hits=0 a=0Call f(0): the fewest coins for amount 0.
9L3calls=7 cache hits=0 a=0Amount 0 needs 0 coins.
10L12calls=7 cache hits=0 a=1f(1) = 1. Stored in the memo.
11L12calls=7 cache hits=0 a=2f(2) = 2. Stored in the memo.
12L2calls=8 cache hits=0 a=0Call f(0): the fewest coins for amount 0.
13L3calls=8 cache hits=0 a=0Amount 0 needs 0 coins.
14L12calls=8 cache hits=0 a=3f(3) = 1. Stored in the memo.
15L2calls=9 cache hits=0 a=1Call f(1): the fewest coins for amount 1.
16L4calls=9 cache hits=1 a=1Cache hit: f(1) = 1 is already known, so nothing below it is recomputed.
17L2calls=10 cache hits=1 a=0Call f(0): the fewest coins for amount 0.
18L3calls=10 cache hits=1 a=0Amount 0 needs 0 coins.
19L12calls=10 cache hits=1 a=4f(4) = 1. Stored in the memo.
20L2calls=11 cache hits=1 a=2Call f(2): the fewest coins for amount 2.
21L4calls=11 cache hits=2 a=2Cache hit: f(2) = 2 is already known, so nothing below it is recomputed.
22L2calls=12 cache hits=2 a=1Call f(1): the fewest coins for amount 1.
23L4calls=12 cache hits=3 a=1Cache hit: f(1) = 1 is already known, so nothing below it is recomputed.
24L12calls=12 cache hits=3 a=5f(5) = 2. Stored in the memo.
25L2calls=13 cache hits=3 a=3Call f(3): the fewest coins for amount 3.
26L4calls=13 cache hits=4 a=3Cache hit: f(3) = 1 is already known, so nothing below it is recomputed.
27L2calls=14 cache hits=4 a=2Call f(2): the fewest coins for amount 2.
28L4calls=14 cache hits=5 a=2Cache hit: f(2) = 2 is already known, so nothing below it is recomputed.
29L12calls=14 cache hits=5 a=6f(6) = 2. Stored in the memo.
30L12return=2 calls=14 cache hits=5Fewest 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.
23.03.3

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 input

Your input

Try
Call

Top-down recursion with a cache: each amount is solved at most once.

Step 1 / 30
Java
1static int[] memo; // memo[a]: -2 = not computed yet, -1 = impossible, otherwise the fewest coins
2static int f(int[] coins, int a, boolean useMemo) {
3 if (a == 0) return 0; // base case
4 if (useMemo && memo[a] != -2) return memo[a]; // cache hit: this state is already solved (even if it was impossible)
5 int best = -1;
6 for (int c : coins) {
7 if (c > a) continue;
8 int sub = f(coins, a - c, useMemo);
9 if (sub >= 0 && (best < 0 || sub + 1 < best)) best = sub + 1;
10 }
11 if (useMemo) memo[a] = best; // store the answer, including -1
12 return best;
13}

Live state

0 calls0 cache hits

calls

0

cache hits

0

amount

6

No condition evaluated on this step

Call stack (top first)

  1. empty
Execution trace (dry run table)Show 30 steps
StepLineStateWhat happens
1L1calls=0 cache hits=0 amount=6Top-down recursion with a cache: each amount is solved at most once.
2L2calls=1 cache hits=0 a=6Call f(6): the fewest coins for amount 6.
3L2calls=2 cache hits=0 a=5Call f(5): the fewest coins for amount 5.
4L2calls=3 cache hits=0 a=4Call f(4): the fewest coins for amount 4.
5L2calls=4 cache hits=0 a=3Call f(3): the fewest coins for amount 3.
6L2calls=5 cache hits=0 a=2Call f(2): the fewest coins for amount 2.
7L2calls=6 cache hits=0 a=1Call f(1): the fewest coins for amount 1.
8L2calls=7 cache hits=0 a=0Call f(0): the fewest coins for amount 0.
9L3calls=7 cache hits=0 a=0Amount 0 needs 0 coins.
10L12calls=7 cache hits=0 a=1f(1) = 1. Stored in the memo.
11L12calls=7 cache hits=0 a=2f(2) = 2. Stored in the memo.
12L2calls=8 cache hits=0 a=0Call f(0): the fewest coins for amount 0.
13L3calls=8 cache hits=0 a=0Amount 0 needs 0 coins.
14L12calls=8 cache hits=0 a=3f(3) = 1. Stored in the memo.
15L2calls=9 cache hits=0 a=1Call f(1): the fewest coins for amount 1.
16L4calls=9 cache hits=1 a=1Cache hit: f(1) = 1 is already known, so nothing below it is recomputed.
17L2calls=10 cache hits=1 a=0Call f(0): the fewest coins for amount 0.
18L3calls=10 cache hits=1 a=0Amount 0 needs 0 coins.
19L12calls=10 cache hits=1 a=4f(4) = 1. Stored in the memo.
20L2calls=11 cache hits=1 a=2Call f(2): the fewest coins for amount 2.
21L4calls=11 cache hits=2 a=2Cache hit: f(2) = 2 is already known, so nothing below it is recomputed.
22L2calls=12 cache hits=2 a=1Call f(1): the fewest coins for amount 1.
23L4calls=12 cache hits=3 a=1Cache hit: f(1) = 1 is already known, so nothing below it is recomputed.
24L12calls=12 cache hits=3 a=5f(5) = 2. Stored in the memo.
25L2calls=13 cache hits=3 a=3Call f(3): the fewest coins for amount 3.
26L4calls=13 cache hits=4 a=3Cache hit: f(3) = 1 is already known, so nothing below it is recomputed.
27L2calls=14 cache hits=4 a=2Call f(2): the fewest coins for amount 2.
28L4calls=14 cache hits=5 a=2Cache hit: f(2) = 2 is already known, so nothing below it is recomputed.
29L12calls=14 cache hits=5 a=6f(6) = 2. Stored in the memo.
30L12return=2 calls=14 cache hits=5Fewest 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.
23.03.4

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.

Java
1// run a deep recursion on a thread with a 256 MB stack
2static long runDeep(int n) throws InterruptedException {
3 long[] result = new long[1];
4 Thread t = new Thread(null, () -> result[0] = depthSum(n), "deep", 256L << 20);
5 t.start();
6 t.join();
7 return result[0];
8}
9
10static long depthSum(int n) {
11 return n == 0 ? 0 : n + depthSum(n - 1); // recursion depth n
12}

Fibonacci with memoization

Interactive · edit the input

Your input

Try

Recursion tree · 1 call

fib(6)…
Running now
Call

Call fib(6).

Step 1 / 52
Java
1static long[] memo = new long[100];
2static long fib(int n) {
3 if (n <= 1) return n; // base cases
4 if (memo[n] != 0) return memo[n]; // already solved: reuse it
5 return memo[n] = fib(n - 1) + fib(n - 2); // solve once, remember
6}

Live state

1 calls

n

6

depth

1

No condition evaluated on this step

Call stack (top first)

  1. fib(6)
Execution trace (dry run table)Show 52 steps
StepLineStateWhat happens
1L2n=6 depth=1Call fib(6).
2L3n=6Not a base case.
3L4n=6 memo[n]=0memo[6] is empty, so compute it.
4L5n=6fib(6) = fib(5) + fib(4). Call fib(5) first.
5L2n=5 depth=2Call fib(5).
6L3n=5Not a base case.
7L4n=5 memo[n]=0memo[5] is empty, so compute it.
8L5n=5fib(5) = fib(4) + fib(3). Call fib(4) first.
9L2n=4 depth=3Call fib(4).
10L3n=4Not a base case.
11L4n=4 memo[n]=0memo[4] is empty, so compute it.
12L5n=4fib(4) = fib(3) + fib(2). Call fib(3) first.
13L2n=3 depth=4Call fib(3).
14L3n=3Not a base case.
15L4n=3 memo[n]=0memo[3] is empty, so compute it.
16L5n=3fib(3) = fib(2) + fib(1). Call fib(2) first.
17L2n=2 depth=5Call fib(2).
18L3n=2Not a base case.
19L4n=2 memo[n]=0memo[2] is empty, so compute it.
20L5n=2fib(2) = fib(1) + fib(0). Call fib(1) first.
21L2n=1 depth=6Call fib(1).
22L3n=1Base case: fib(1) = 1.
23L3n=1 return=1return 1.
24L5n=2 left=1fib(1) = 1. Now call fib(0).
25L2n=0 depth=6Call fib(0).
26L3n=0Base case: fib(0) = 0.
27L3n=0 return=0return 0.
28L5n=2 left=1 right=0 return=1fib(2) = 1 + 0 = 1. Store it in memo[2].
29L5n=3 left=1fib(2) = 1. Now call fib(1).
30L2n=1 depth=5Call fib(1).
31L3n=1Base case: fib(1) = 1.
32L3n=1 return=1return 1.
33L5n=3 left=1 right=1 return=2fib(3) = 1 + 1 = 2. Store it in memo[3].
34L5n=4 left=2fib(3) = 2. Now call fib(2).
35L2n=2 depth=4Call fib(2).
36L3n=2Not a base case.
37L4n=2 memo[n]=1memo[2] is already 1. No need to recurse again.
38L4n=2 return=1Cache hit: return 1 immediately. This whole subtree is skipped.
39L5n=4 left=2 right=1 return=3fib(4) = 2 + 1 = 3. Store it in memo[4].
40L5n=5 left=3fib(4) = 3. Now call fib(3).
41L2n=3 depth=3Call fib(3).
42L3n=3Not a base case.
43L4n=3 memo[n]=2memo[3] is already 2. No need to recurse again.
44L4n=3 return=2Cache hit: return 2 immediately. This whole subtree is skipped.
45L5n=5 left=3 right=2 return=5fib(5) = 3 + 2 = 5. Store it in memo[5].
46L5n=6 left=5fib(5) = 5. Now call fib(4).
47L2n=4 depth=2Call fib(4).
48L3n=4Not a base case.
49L4n=4 memo[n]=3memo[4] is already 3. No need to recurse again.
50L4n=4 return=3Cache hit: return 3 immediately. This whole subtree is skipped.
51L5n=6 left=5 right=3 return=8fib(6) = 5 + 3 = 8. Store it in memo[6].
52L5return=8fib(6) = 8 with 11 calls. Plain recursion would need 25 calls.
23.03.5

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.

n56810
Fibonacci calls, plain recursion152567177
Fibonacci calls, memoised9111519

Top-down coin change: plain recursion vs memoization

Interactive · edit the input

Your input

Try
Call

Top-down recursion with a cache: each amount is solved at most once.

Step 1 / 30
Java
1static int[] memo; // memo[a]: -2 = not computed yet, -1 = impossible, otherwise the fewest coins
2static int f(int[] coins, int a, boolean useMemo) {
3 if (a == 0) return 0; // base case
4 if (useMemo && memo[a] != -2) return memo[a]; // cache hit: this state is already solved (even if it was impossible)
5 int best = -1;
6 for (int c : coins) {
7 if (c > a) continue;
8 int sub = f(coins, a - c, useMemo);
9 if (sub >= 0 && (best < 0 || sub + 1 < best)) best = sub + 1;
10 }
11 if (useMemo) memo[a] = best; // store the answer, including -1
12 return best;
13}

Live state

0 calls0 cache hits

calls

0

cache hits

0

amount

6

No condition evaluated on this step

Call stack (top first)

  1. empty
Execution trace (dry run table)Show 30 steps
StepLineStateWhat happens
1L1calls=0 cache hits=0 amount=6Top-down recursion with a cache: each amount is solved at most once.
2L2calls=1 cache hits=0 a=6Call f(6): the fewest coins for amount 6.
3L2calls=2 cache hits=0 a=5Call f(5): the fewest coins for amount 5.
4L2calls=3 cache hits=0 a=4Call f(4): the fewest coins for amount 4.
5L2calls=4 cache hits=0 a=3Call f(3): the fewest coins for amount 3.
6L2calls=5 cache hits=0 a=2Call f(2): the fewest coins for amount 2.
7L2calls=6 cache hits=0 a=1Call f(1): the fewest coins for amount 1.
8L2calls=7 cache hits=0 a=0Call f(0): the fewest coins for amount 0.
9L3calls=7 cache hits=0 a=0Amount 0 needs 0 coins.
10L12calls=7 cache hits=0 a=1f(1) = 1. Stored in the memo.
11L12calls=7 cache hits=0 a=2f(2) = 2. Stored in the memo.
12L2calls=8 cache hits=0 a=0Call f(0): the fewest coins for amount 0.
13L3calls=8 cache hits=0 a=0Amount 0 needs 0 coins.
14L12calls=8 cache hits=0 a=3f(3) = 1. Stored in the memo.
15L2calls=9 cache hits=0 a=1Call f(1): the fewest coins for amount 1.
16L4calls=9 cache hits=1 a=1Cache hit: f(1) = 1 is already known, so nothing below it is recomputed.
17L2calls=10 cache hits=1 a=0Call f(0): the fewest coins for amount 0.
18L3calls=10 cache hits=1 a=0Amount 0 needs 0 coins.
19L12calls=10 cache hits=1 a=4f(4) = 1. Stored in the memo.
20L2calls=11 cache hits=1 a=2Call f(2): the fewest coins for amount 2.
21L4calls=11 cache hits=2 a=2Cache hit: f(2) = 2 is already known, so nothing below it is recomputed.
22L2calls=12 cache hits=2 a=1Call f(1): the fewest coins for amount 1.
23L4calls=12 cache hits=3 a=1Cache hit: f(1) = 1 is already known, so nothing below it is recomputed.
24L12calls=12 cache hits=3 a=5f(5) = 2. Stored in the memo.
25L2calls=13 cache hits=3 a=3Call f(3): the fewest coins for amount 3.
26L4calls=13 cache hits=4 a=3Cache hit: f(3) = 1 is already known, so nothing below it is recomputed.
27L2calls=14 cache hits=4 a=2Call f(2): the fewest coins for amount 2.
28L4calls=14 cache hits=5 a=2Cache hit: f(2) = 2 is already known, so nothing below it is recomputed.
29L12calls=14 cache hits=5 a=6f(6) = 2. Stored in the memo.
30L12return=2 calls=14 cache hits=5Fewest 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.