skip to content

Why doesn't memoization turn every exponential recursion into a polynomial algorithm?

level: seniorimportance: nice to knowfreq 30%

answer

  1. what does a cache actually remove?
  2. states times work per state
  3. how many keys can exist?
  4. a visited set is a subset
  5. shrinking the key breaks correctness

basics

~20 s

Memoization caps work at one evaluation per distinct state, so running time is the state count times work per state. When the state includes a set of already-visited places, that count is exponential and caching saves nothing asymptotically.

solid answer

~50 s

Memoization does not make a recursion fast; it caps its work at **one evaluation per distinct state**. So the running time is approximately `(number of distinct states) x (work per state)`, and the payoff depends entirely on how big that state space is. When a route search must remember which places it has already visited, the honest state is `(current place, visited set)`, and the number of visited sets grows with the number of subsets of places — exponential. Caching removes duplicate evaluations and still leaves exponentially many distinct ones, plus exponential memory. The second failure is worse and quieter: if you shrink the key to make the space small — memoizing on the current place alone — the cached answer no longer depends only on the key, so you get fast **wrong** answers. Sizing the state space is therefore something you do before writing the cache, not after.

code

pseudocode · 12 lines
pseudocode
BEST(u, target, visited):
    if u == target:
        return 0
    best = -INFINITY
    for v in neighbors(u):
        if v not in visited:
            best = max(best, w(u, v) + BEST(v, target, visited + {v}))
    return best

// initial call: BEST(source, target, {source})
// memo key must be (u, visited): the answer depends on both
// distinct states = places x subsets of places  -> exponential

go deeper

for a junior

Understand that a cache saves you from answering the same question twice, and that its benefit therefore depends on how many different questions exist rather than on how deep the recursion goes.

for a middle

Explain the cost model out loud: running time is roughly the number of distinct states times the work per state, and memory is the state count. Show you can compute both from the parameter ranges.

for a senior

Demonstrate that you size the state space before coding, that you can spot a key missing a dependency, and that you know a truncated key produces fast wrong answers rather than slow right ones.

for a principal

Own the call when the exact formulation is exponential: whether to bound the input so an exact method is safe, invest in a heuristic with a stated quality bound, or reshape the problem — and make the ceiling explicit rather than implicit.

## The claim being corrected "Any exponential recursion can be memoized into polynomial time" is one of the most confidently stated wrong answers in this area. It survives because the examples people learn on — a numeric recurrence, a two-index sequence problem — happen to have small state spaces, so the cache really does collapse the work. The collapse is a property of those problems, not of caching. ## The formula that settles it For a memoized recursion: ``` time ~= (number of distinct reachable states) x (work per state, excluding recursive calls) space ~= (number of distinct stored states) + (recursion depth) ``` A cache guarantees each distinct state is evaluated once. That is its entire contribution. It says nothing about how many distinct states exist — and that count is fixed by how you identify a subproblem, which is fixed by what the answer actually depends on. For the numeric case where a subproblem is named by a single integer up to `n`, there are about `n` states, each doing constant work, so a call tree that blew up becomes linear. Nothing mysterious happened: the state space was always small, and the cache simply stopped the recursion from asking the same small set of questions repeatedly. ## The case where it does not collapse Consider searching a road network for the highest-value route from a start to a destination that never revisits a place. Whether a continuation is legal depends on which places have already been used, so the subproblem must be identified by both: ``` BEST(u, target, visited): if u == target: return 0 best = -INFINITY for v in neighbors(u): if v not in visited: best = max(best, w(u, v) + BEST(v, target, visited + {v})) return best // memo key must be the pair (u, visited) -- not u alone ``` Now count states. The first component ranges over places; the second ranges over *subsets* of places. The product grows exponentially with the number of places. Memoizing is not wrong — the cache does earn hits, because the same `(place, visited set)` pair is genuinely reachable by different orderings of the same set — but one evaluation per distinct state is still exponentially many evaluations, and the table itself needs exponential memory, which usually becomes the binding constraint long before the clock does. This is why the subproblem space, not the recursion, determines the complexity class. Memoization is a technique for eliminating *redundancy*; it cannot eliminate *distinct required work*. ## The quieter failure: shrinking the key Facing an unaffordable table, the tempting move is to drop part of the key — memoize on the current place alone. The state space becomes small and the algorithm becomes fast, and it also becomes incorrect. Caching is only valid when the stored answer is a **function of the key**: the same key must always imply the same answer. The best continuation from a place depends on which places remain available, so two visits with different visited sets have different answers under one key. The cache serves the first one to both. This bug is nastier than a slow algorithm because it does not announce itself. Small test graphs often have few enough alternative routes that the wrong cached answer coincides with the right one; the discrepancy appears on larger inputs, intermittently, and looks like a data problem rather than a design error. When reviewing a memoized recursion, the check is mechanical: **list everything the returned value depends on, and confirm every one of those things appears in the key.** ## What to do instead - **Size the state space first.** Multiply out the ranges of the parameters before writing a line. If the product is exponential, memoizing is not the fix and no amount of tuning will make it one. - **Look for state you can legitimately drop.** Sometimes a parameter is redundant — derivable from the others, or provably irrelevant to the answer. Dropping such a parameter is sound and shrinks the space genuinely; dropping a parameter the answer depends on is the bug above. - **Accept exponential deliberately when the input is bounded.** A subset-indexed table is a real and respectable technique when the element count is small and fixed; the decision is a capacity decision, made with the ceiling written down. - **Change the problem.** Approximation, a heuristic search with pruning, or a reformulation that removes the global constraint are the usual routes once the exact state space is known to be unaffordable. ## The one-line version Memoization bounds work by the size of the subproblem space. Polynomial time follows only when that space is polynomial — and when it is not, the choice is an exponential cost you accepted knowingly or a cache key that lies.

  • How do you size a memoized recursion's cost before implementing it?
    Write down every parameter in the memo key, multiply out their ranges to get the state count, then multiply by the work each state does outside its recursive calls. That product is the running time, and the state count alone is the memory. A key containing a set or a permutation is the signal that the product is exponential, which is a design decision rather than a tuning problem.
  • How would you detect a memo key that is missing part of the state?
    List every input the returned value depends on and check each appears in the key. In testing, run the same computation with caching disabled and compare results on inputs large enough to have multiple routes to the same key; a divergence pinpoints the missing dimension. The failure is intermittent and input-dependent, so equality on small cases proves nothing.
  • Is an exponential state space ever an acceptable answer?
    Yes, when the parameter driving it is small and bounded by the domain rather than by the data. A subset-indexed table over a dozen items is entirely practical and exact. What makes it acceptable is stating the ceiling explicitly and knowing what happens at that ceiling, rather than discovering it when memory runs out in production.

A cache is a promise not to answer the same question twice. If you have exponentially many different questions to answer, keeping that promise perfectly still leaves you with exponentially much work.

saying these in an interview costs you the question

  • Says memoization always yields polynomial time
  • Confuses number of recursive calls with number of distinct states
  • Drops part of the key to shrink the table
  • Ignores that the memo table itself costs memory
  • Never estimates the state space before implementing
  • Assumes a wrong cache key only costs speed, not correctness

context