13.3 Memoization

Once a state and recurrence have been defined, the most direct way to implement a dynamic programming solution is memoization.

13.3 Memoization

Once a state and recurrence have been defined, the most direct way to implement a dynamic programming solution is memoization. Memoization starts from the original recursive formulation of a problem and adds a cache that remembers previously computed results. Whenever the same subproblem appears again, the stored answer is returned immediately instead of recomputing it.

Memoization is often the easiest way to convert an exponential recursive algorithm into a polynomial-time dynamic programming solution. It preserves the natural structure of the recurrence while eliminating redundant work.

Problem

Given a recursive recurrence relation with overlapping subproblems, how can we avoid recomputing identical states?

Understanding Overlapping Subproblems

Consider the Fibonacci recurrence:

$$ F(n)=F(n-1)+F(n-2) $$

A naive recursive implementation repeatedly solves the same subproblems.

For example:

F(6)
├── F(5)
│   ├── F(4)
│   │   ├── F(3)
│   │   └── F(2)
│   └── F(3)
└── F(4)
    ├── F(3)
    └── F(2)

Notice that:

F(4)
F(3)
F(2)

appear multiple times.

The recursive algorithm performs the same computations repeatedly.

The number of calls grows exponentially.

The Core Idea

Store the result of each state after computing it.

When the state is requested again:

return cached value

instead of:

recompute recursively

This simple modification transforms many exponential algorithms into polynomial-time solutions.

Recipe 1: Cache Every State

Start with the recursive recurrence.

For Fibonacci:

$$ F(n)=F(n-1)+F(n-2) $$

Add a cache:

var memo map[int]int

func fib(n int) int {
    if n <= 1 {
        return n
    }

    if v, ok := memo[n]; ok {
        return v
    }

    memo[n] = fib(n-1) + fib(n-2)
    return memo[n]
}

Each state is computed once.

All future requests become constant-time lookups.

Complexity Transformation

Without memoization:

$$ T(n)=T(n-1)+T(n-2) $$

Complexity:

$$ O(2^n) $$

With memoization:

States:

$$ n+1 $$

Each state computed once:

$$ O(n) $$

The improvement is dramatic.

Example: Staircase Problem

Problem:

You may climb 1 or 2 steps.
Count ways to reach step n.

State:

$$ dp[i] $$

Meaning:

number of ways to reach step i

Recurrence:

$$ dp[i] = dp[i-1] + dp[i-2] $$

Memoized implementation:

func ways(i int, memo map[int]int) int {
    if i <= 1 {
        return 1
    }

    if v, ok := memo[i]; ok {
        return v
    }

    memo[i] = ways(i-1, memo) + ways(i-2, memo)

    return memo[i]
}

Every staircase size is evaluated exactly once.

Example: Coin Change

Problem:

Minimum coins needed
to form amount x

State:

$$ dp[x] $$

Recurrence:

$$ dp[x] = 1+ \min(dp[x-c]) $$

for every valid coin value (c).

Memoized implementation:

func solve(amount int) int {
    if amount == 0 {
        return 0
    }

    if amount < 0 {
        return inf
    }

    if v, ok := memo[amount]; ok {
        return v
    }

    best := inf

    for _, coin := range coins {
        best = min(
            best,
            solve(amount-coin)+1,
        )
    }

    memo[amount] = best
    return best
}

Large recursion trees collapse into a small collection of cached states.

Example: Longest Common Subsequence

State:

$$ dp[i][j] $$

Meaning:

LCS length
between prefixes
A[0...i]
B[0...j]

Recurrence:

If characters match:

$$ dp[i][j] = 1+dp[i-1][j-1] $$

Otherwise:

$$ dp[i][j] = \max ( dp[i-1][j], dp[i][j-1] ) $$

Memoization stores each pair:

(i,j)

after its first computation.

Instead of exploring exponentially many recursive paths, the algorithm evaluates at most:

$$ O(nm) $$

states.

Memoization and State Graphs

Every dynamic programming problem can be viewed as a graph.

Vertices:

states

Edges:

transitions

Without memoization:

same vertex visited repeatedly

With memoization:

each vertex processed once

Memoization performs a depth-first traversal of the state graph while caching results.

Memoization Template

Most memoized solutions follow the same structure.

func solve(state State) Result {

    if isBaseCase(state) {
        return baseAnswer
    }

    if value, found := memo[state]; found {
        return value
    }

    answer := combine(
        solve(smallerState1),
        solve(smallerState2),
        ...
    )

    memo[state] = answer

    return answer
}

This pattern appears throughout dynamic programming.

Choosing a Cache Structure

Arrays

When state indices are contiguous.

Example:

memo := make([]int, n+1)

Advantages:

  • Fastest access
  • Excellent cache locality
  • Minimal overhead

Hash Maps

When states are sparse.

Example:

memo := make(map[State]int)

Advantages:

  • Flexible
  • Handles arbitrary states
  • Memory efficient for sparse state spaces

Multidimensional Arrays

Example:

memo := make([][]int, n)

Useful for:

  • grids
  • sequences
  • interval DP

Detecting Memoization Opportunities

Memoization works best when both conditions hold.

Optimal Substructure

Solutions depend on smaller solutions.

Example:

shortest path
knapsack
LCS

Overlapping Subproblems

Same states appear repeatedly.

Example:

F(5)

may appear dozens of times during recursion.

Without overlap, memoization provides little benefit.

Memoization vs Ordinary Caching

Memoization is a specialized form of caching.

Ordinary caching:

store arbitrary results

Memoization:

store results of pure subproblems

A memoized state should always produce the same answer.

This deterministic property guarantees correctness.

Common Mistakes

Forgetting to Store Results

solve(...)

is computed repeatedly.

Performance remains exponential.

Incorrect State Keys

Example:

store only position

when answer also depends on:

remaining capacity

Different subproblems overwrite each other.

Mutable State

If a state contains mutable data:

lists
maps
objects

hashing and equality become error-prone.

Prefer immutable state representations.

Missing Base Cases

Memoization does not eliminate the need for valid stopping conditions.

Infinite recursion still occurs.

Memory Considerations

Memoization stores every reachable state.

Memory complexity:

$$ O(S) $$

where:

$$ S $$

is the number of distinct states.

Sometimes:

time decreases dramatically
memory increases significantly

This tradeoff is fundamental to dynamic programming.

Memoization Advantages

Natural Implementation

The code mirrors the mathematical recurrence.

Easy to Write

No dependency ordering is required.

Computes Only Needed States

Unreachable states are never evaluated.

Excellent for Complex State Spaces

Tree DP, graph DP, digit DP, and game search often begin with memoization.

Memoization Disadvantages

Recursion Overhead

Function calls introduce extra cost.

Stack Depth Limits

Deep recursion may overflow the call stack.

Less Predictable Memory Access

Cached states may be visited in irregular order.

Harder to Optimize

Advanced space optimizations are usually easier with tabulation.

Complexity Analysis

Let:

$$ S $$

be the number of distinct states.

Let:

$$ K $$

be the transition cost.

Each state is computed once.

Total running time:

$$ O(S \times K) $$

Memory usage:

$$ O(S) $$

These formulas characterize most memoized dynamic programming algorithms.

Takeaway

Memoization transforms recursive recurrences into efficient dynamic programming solutions by caching previously computed states. Each state is evaluated at most once, eliminating redundant computation caused by overlapping subproblems. The resulting implementation closely follows the mathematical recurrence, making memoization one of the most effective and intuitive ways to develop dynamic programming solutions before moving to iterative tabulation techniques.