Cloud Tech by Victor
AlgorithmsIntermediate

Dynamic Programming

Why caching subproblem solutions turns an exponential naive recursive Fibonacci into a linear one, and what "overlapping subproblems" actually has to be true about a problem before memoization can help at all.

Updated 2026-07-243 min read

Overview

Dynamic programming, per NIST's own definition, solves an optimization problem by caching subproblem solutions (memoization) instead of recomputing them, and it only actually helps when a problem has overlapping subproblems, the same smaller subproblem genuinely recurring across different branches of the larger recursive structure. Naive recursive Fibonacci is the canonical example: fib(n-2) gets computed once directly and again inside the fib(n-1) branch, and that duplication compounds into roughly 2^n total calls; caching each result the first time collapses it to O(n), one computation per distinct subproblem. A problem without overlapping subproblems, like standard mergesort where every recursive call operates on a genuinely distinct array slice that never recurs, gets zero benefit from memoization, there's nothing to cache, only overhead to add, which is exactly why recognizing overlapping subproblems is the real prerequisite before reaching for this technique.

Quick Reference

ConceptMeaning
MemoizationCaching a subproblem's result so later occurrences are a lookup, not a recomputation
Overlapping subproblemsThe same subproblem genuinely recurs across different branches of the recursion
Optimal substructureAn optimal solution can be built from optimal solutions to its subproblems
No overlap → no benefitMemoization adds only overhead when subproblems never actually repeat

Syntax

python
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

Examples

python
# Without memoization: ~2^n calls, fib(n-2) recomputed repeatedly.
def fib_naive(n):
    if n < 2: return n
    return fib_naive(n - 1) + fib_naive(n - 2)

# With memoization: O(n), each distinct subproblem solved once.
def fib_memo(n, cache={}):
    if n < 2: return n
    if n not in cache:
        cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
    return cache[n]

Check for overlapping subproblems before reaching for memoization

If the recursive breakdown never revisits the same subproblem twice (like mergesort's array slices), there's nothing for a cache to reuse; memoization adds storage and lookup overhead with zero benefit in that case.

Visual Diagram

Common Mistakes

  • Applying memoization to a problem without overlapping subproblems, adding cache overhead for zero actual reuse benefit.
  • Writing a naive recursive solution to a problem known to have overlapping subproblems (Fibonacci, edit distance, knapsack) and being surprised by exponential blowup on larger inputs.
  • Caching on the wrong key, memoizing by object identity or an incomplete parameter set instead of everything that actually determines a subproblem's result.
  • Confusing dynamic programming (caching optimal subproblem solutions) with plain recursion or with greedy algorithms, which don't reconsider earlier choices at all.

Performance

  • Memoization trades space (the cache) for time, turning exponential repeated recomputation into linear-or-polynomial time proportional to the number of distinct subproblems, not the number of recursive calls made.
  • The size of the cache scales with the number of distinct subproblems, which is itself bounded by the problem's actual parameter space, not by how many times a subproblem happens to recur.

Best Practices

  • Confirm a problem actually has overlapping subproblems before reaching for memoization; without that property, it's pure overhead.
  • Cache on the complete set of parameters that determine a subproblem's result, not a subset that could cause incorrect cache hits.
  • Prefer a language's built-in memoization utility (like functools.lru_cache) over a hand-rolled cache dictionary when it fits, less code to get wrong.
  • Consider iterative "tabulation" (building up from the smallest subproblems) as an alternative to recursive memoization when recursion depth on the naive version would be a concern.

Interview questions

What does NIST's own definition of dynamic programming actually say the technique does, and what problem does it solve?

NIST's Dictionary of Algorithms and Data Structures defines dynamic programming as an algorithmic technique to "solve an optimization problem by caching subproblem solutions (memoization) rather than recomputing them." The problem it solves is redundant recomputation: when a naive recursive solution calls itself with the same subproblem arguments repeatedly (matrix-chain multiplication, longest common subsequence, and similar problems are the examples NIST gives), that same subproblem gets solved from scratch every single time it recurs, and caching the first result lets every later occurrence be a lookup instead of a full recomputation.

Why does a naive recursive Fibonacci function run in exponential time, and how does memoization fix that specifically?

Naive recursive `fib(n) = fib(n-1) + fib(n-2)` recomputes the exact same subproblem enormous numbers of times, `fib(n-2)` gets computed once directly and once again inside the `fib(n-1)` call, and this duplication compounds recursively, producing roughly 2^n total calls. Memoization caches each `fib(k)` result the first time it's computed, so every subsequent call with the same `k` becomes an O(1) cache lookup instead of a full recursive recomputation, collapsing the total distinct work down to O(n), one computation per distinct subproblem instead of an exponential number of repeated ones.

What does "overlapping subproblems" mean, and why does dynamic programming provide no benefit for a problem that lacks it?

Overlapping subproblems means the same smaller subproblem genuinely recurs multiple times across different branches of the larger problem's recursive structure, exactly what makes caching valuable, the second and later occurrences become free lookups. A problem like standard mergesort, by contrast, has no overlapping subproblems, every recursive call operates on a genuinely distinct slice of the array that never recurs anywhere else, so there is nothing to cache and memoization adds only overhead (cache storage and lookup cost) with zero reuse to offset it. Recognizing whether a problem's recursive breakdown actually revisits the same subproblems is the real prerequisite for dynamic programming to help at all.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement