Dynamic Programming
Intermediate 11 min read

Memoization vs tabulation

In one line

Dynamic programming solves a problem by solving each overlapping subproblem once and reusing the answer, either top-down with a cache (memoization) or bottom-up by filling a table (tabulation).

What it is

Dynamic programming (DP) applies when a problem has optimal substructure (the answer can be built from answers to smaller subproblems) and overlapping subproblems (the same smaller subproblems are needed again and again). Plain recursion recomputes them; DP computes each one once.

Memoization is top-down: write the natural recursive function, and store each result in a cache keyed by its arguments, so a repeated call returns instantly. Tabulation is bottom-up: decide an order in which subproblems depend only on earlier ones, then fill an array or table from the base cases up with loops.

Both give the same time complexity, usually (number of states) x (work per state). They differ in style: memoization only computes the states actually needed and is easier to write first; tabulation avoids recursion depth limits, has less overhead, and often lets you shrink memory to the last row or last few values.

Why interviewers ask

DP is one of the most common hard interview topics. Interviewers want to see a clear method: define the state, write the recurrence, set the base cases, choose the order, then optimise space. Being able to move from a brute-force recursion to memoization to a tabulated, space-optimised version is exactly the progression they look for.

How it works

  1. 1Write a brute-force recursion and notice repeated calls with the same arguments; that is the overlapping-subproblem signal.
  2. 2Define the state precisely in words, for example 'f(i) = the answer for the first i items'. The arguments that change between calls are the state.
  3. 3Write the recurrence (how f(i) is built from smaller states) and the base cases.
  4. 4Memoize: add a cache (a dict or functools.lru_cache). Each state is computed once, so time = states x transitions.
  5. 5Or tabulate: create dp sized to the state space, fill base cases, loop in an order where dependencies come first, and return the target cell.
  6. 6Optimise space: if dp[i] only uses dp[i-1] and dp[i-2], keep two variables instead of the whole array.

Worked example · Fibonacci: from 25 calls to 7 steps

  1. fib(n) = fib(n-1) + fib(n-2), with fib(0) = 0 and fib(1) = 1. Plain recursion for fib(6) makes 25 calls, because fib(2) alone is computed 5 times; it grows exponentially, about O(1.6^n).
  2. Memoized: fib(6) calls fib(5), which calls fib(4) and so on down to fib(1) and fib(0). On the way back up, each fib(k) is stored. When fib(6) then asks for fib(4), it is already in the cache. Only 7 distinct values (fib(0) to fib(6)) are computed: O(n).
  3. Tabulated: dp = [0, 1, _, _, _, _, _]. i=2: dp[2] = 1 + 0 = 1. i=3: 2. i=4: 3. i=5: 5. i=6: 8.
  4. Space-optimised: keep a, b = 0, 1 and repeat a, b = b, a + b six times: (1,1), (1,2), (2,3), (3,5), (5,8), (8,13). Answer a = 8, with O(1) memory.
  5. All three versions return 8; they differ only in order of work and memory used.

Picture it

Memoization is a student who solves homework questions as they come up and writes every answer in a notebook so they never redo one. Tabulation is a student who works through the textbook in order, chapter by chapter, so every later problem can use answers already written down.

Reference code

from functools import lru_cache


@lru_cache(maxsize=None)
def fib_memo(n):
    """Top-down: plain recursion plus a cache."""
    if n < 2:
        return n
    return fib_memo(n - 1) + fib_memo(n - 2)


def fib_table(n):
    """Bottom-up: fill the table from the base cases."""
    if n < 2:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]


def fib_O1(n):
    """Bottom-up keeping only the last two values."""
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

# fib_memo(6) == fib_table(6) == fib_O1(6) == 8

The same recurrence three ways: memoized recursion (O(n) time and stack), a tabulated array (O(n) time and space), and a rolling pair of variables (O(n) time, O(1) space).

Key patterns

Brute force, then memoize

When: You can write a recursion but it is too slow

How: Find the arguments that change, cache results by those arguments, and count states x transitions for complexity.

Tabulate for large inputs

When: Recursion depth could exceed limits (Python defaults to about 1000 frames)

How: Fill the table in dependency order with loops.

Rolling arrays

When: Each row or value depends only on the previous one or two

How: Keep only the last row or last two values to cut memory from O(n) to O(1), or O(m x n) to O(n).

Reconstruct the answer

When: You need the actual choices, not just the optimal value

How: Store a parent or choice per state, or walk back through the full table from the final cell.

Real-world use

Text diff and version control

Diff tools compare files using algorithms built on longest common subsequence and shortest edit scripts; git's default diff is Myers' algorithm, a refined dynamic-programming style search over edit paths.

Spell checking and search

Spell checkers and fuzzy search (for example 'did you mean' suggestions and fuzzy matching in search engines like Elasticsearch) use edit distance, a 2D DP table.

Bioinformatics

Sequence alignment tools use the Needleman-Wunsch (global) and Smith-Waterman (local) algorithms, which are DP tables over two DNA or protein sequences.

Speech recognition and decoding

The Viterbi algorithm, a DP over time steps and hidden states, finds the most likely sequence of states in hidden Markov models and is used in speech recognition and in decoding convolutional codes in mobile networks.

In a project

A pricing service computes the cheapest way to fulfil a customer's order from bundles (for example 1, 6 and 20 packs with different prices). The first version is a recursive search that times out for orders above 60 units.

  1. Identify the state: best(n) = cheapest price for exactly n units. The recursion tries each bundle and calls best(n - size).
  2. Add memoization with a dictionary and confirm that answers match the slow version on small orders.
  3. Convert to tabulation: fill best[0..n] in increasing order so there is no recursion depth limit for large orders.
  4. Store the bundle chosen at each n so the service can reconstruct the actual bundle list, not just the price.

Impact: Pricing goes from exponential time to O(n x bundles), so a 10,000 unit order is priced in milliseconds, and the reconstructed bundle list lets customers see exactly what they will receive.

Trade-offs

Use it when

  • Memoization: the recursion is natural and only some states are reachable
  • Tabulation: all states are needed, input is large, or you want to optimise memory
  • Either: brute force has repeated subproblems and optimal substructure

Avoid it when

  • Subproblems do not overlap (plain divide and conquer like merge sort gains nothing from a cache)
  • A proven greedy rule exists and is simpler and faster
  • The state space is too large to store (for example all subsets of 50 items)

Cost: Time = number of states x work per state; space = number of states stored, often reducible with rolling arrays.

Common mistakes

  • Jumping to a table without defining the state in words; most DP bugs come from a vague state.
  • Missing or wrong base cases, such as dp[0] for an empty prefix.
  • Filling the table in an order where a cell is read before it is computed.
  • Using mutable or unhashable arguments (lists) as memo keys; convert to tuples or indices.
  • Hitting Python's recursion limit with deep memoized recursion instead of switching to tabulation.

Interview questions

Quick revision

  • DP = optimal substructure + overlapping subproblems.
  • Steps: state, recurrence, base cases, order, answer cell, then space.
  • Memoization = recursion + cache (top-down).
  • Tabulation = loops filling a table from base cases (bottom-up).
  • Complexity = states x transitions.
  • Rolling arrays cut space when only the previous row is used.
  • Naive Fibonacci is exponential; DP makes it O(n).

Check yourself

1. Which two properties signal that DP applies?

2. A DP has states (i, j) with i < 1000 and j < 500, and each state does O(1) work. What is the time complexity?

3. What is a main advantage of tabulation over memoization?

4. Why does merge sort not benefit from memoization?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free