skip to content

questions

16

Why does memoizing an exponential recursion change its time complexity, and to what?

level: juniorimportance: must knowfreq 82%

answer

  1. what does the naive recursion repeat?
  2. count distinct subproblems, not call paths
  3. each state computed once, then looked up
  4. total equals states times work per state
  5. no repeats means no savings

basics

~20 s

Memoization computes each distinct subproblem once and reuses the stored result, so cost drops from the number of call paths to (number of distinct states) times (work per transition). It only helps when subproblems actually repeat.

solid answer

~50 s

The naive recursion is expensive because the same subproblem is re-derived along many different call paths. Take a dependency resolver that, for 40 packages, decides include-or-skip at each step: the raw recursion tree has on the order of `2^40` paths. Add a cache keyed by the subproblem's state and each distinct state is computed once and afterwards returned by lookup, so the total becomes (number of reachable distinct states) x (cost of one transition), plus the cost of the lookups themselves. If the state is just "which package index am I at", that is 40 states and the run is trivial; if the state is "index plus the exact set already chosen", the states are still exponential and the cache saves nothing. So the honest answer always names the state space — never just "memoization makes it fast".

go deeper

for a junior

Be ready to say that a cache turns repeated subproblem calls into lookups, and that you count distinct subproblems rather than branches of the recursion tree.

for a middle

Explain the accounting out loud: total time is the number of reachable states times the work one transition does, and state what a single state means in the problem at hand.

for a senior

Show that you verify subproblems genuinely overlap before promising a speedup, and that you size the cache against real limits — entry count, key cost, and the memory it holds at production input sizes.

for a principal

Own the call on whether an exact cached solution is worth its memory at scale, and when a bounded approximation or a different formulation beats a cache that will never fit in the budget you have.

## The two numbers you must say out loud A memoized recursion's running time is, to a very good first approximation: **time = (number of distinct states actually reached) x (cost of computing one state from its dependencies)** plus the cost of the cache operations themselves. Almost every wrong answer in this area comes from quoting only one of those two factors, or from quoting neither and saying "it becomes linear". ## Why the naive version is exponential in the first place Recursion becomes exponential when the recursion *tree* is much bigger than the set of *distinct* arguments in it. Consider resolving a dependency set: you walk a list of 40 candidate packages and at each one decide to include it or skip it, recursing on the rest. The tree of decisions has two children per level and 40 levels, so it contains on the order of `2^40` root-to-leaf paths — about a trillion. That number counts *paths*, not *problems*. Many of those paths arrive at the very same question: "what is the best I can do from package 17 onward, given I have this much budget left?" The exponential blow-up is repeated work, not necessary work. This is the *overlapping subproblems* property. It is a property of the problem plus the chosen state encoding — not something memoization creates. ## What the cache changes A memo is a table keyed by the state. The first time a state is requested you compute it and store it; every later request is a lookup. Charge the work with aggregate accounting: over the whole run, exactly one "compute" happens per distinct reached state, and every other call is a lookup that returns immediately. So: - Number of computes = number of distinct reached states, call it `S`. - Work per compute = the transition cost `T` — the number of dependencies it combines, plus whatever it does per dependency. - Number of lookups = at most `S x (branching factor)`, because every compute issues at most that many child requests; each lookup is expected constant time for an array-indexed table and expected constant for a keyed cache with a good hash. Total: `O(S x T)`. For the 40-package resolver with state "(index, remaining budget)" and an O(1) transition, that is `41 x (budget+1)` computes — an utterly different animal from `2^40`, and the sentence an interviewer wants is exactly that pair of numbers stated side by side. ## The trap: "memoization makes it O(n)" The most common wrong answer names the *problem input size* instead of the *state space*. Two failure shapes: 1. **Multi-dimensional state.** If the state is a pair (position, remaining capacity), `S` is the product of the two ranges, not the length of the input. Saying "O(n)" silently drops a whole dimension. 2. **Non-constant transition.** If computing one state scans all earlier positions, `T` is itself `O(n)` and the product is a factor of `n` bigger than the table size. Counting cache entries is not counting time. A good habit: before quoting a complexity, say the state tuple aloud, count how many values each component can take, multiply, then separately describe what one transition does. ## When memoization does not help at all If no two call paths ever reach the same state, the cache is written to and never read; the asymptotic time is unchanged and you have added memory and hashing overhead for nothing. Enumerating every distinct ordering of a set behaves this way — each path ends somewhere unique. This is why "has overlapping subproblems" is a precondition you check, not a wish. Likewise, if the state must encode an arbitrary subset of the items to be correct, the state space is exponential and memoization gives you an exponential-space algorithm that is still exponential in time — occasionally a real win in practice on small inputs, but never a change of complexity class. ## Space is part of the answer The cache holds up to `S` entries, so the space is `O(S)` for the table plus the recursion stack, whose depth is part of the algorithm's space too. "It runs fast now" is only half a report; interviewers routinely follow up with "and how much memory does that cache hold at your real input sizes?" — a cache with billions of entries is not a solution. ## The one-sentence summary Memoization does not make things fast; it removes *repetition*, converting call-path counting into state counting. What is left is states times transition cost, and you must be able to name both.

  • Your memo ends up with 40 entries. Does that make the algorithm O(40)?
    Only if one entry costs constant time to compute. The running time is entries times transition cost, so a state whose transition scans a list of length m costs `40 x m`. And check the key: if the real state is a pair, the entry count is the product of both ranges, not the length of one input.
  • When does adding a memo leave the asymptotic time unchanged?
    When distinct call paths never collide on the same state, so every cache write is followed by no read. Enumerating all distinct orderings of a set behaves like that. You pay memory and key-hashing cost and get nothing back, which is why you verify overlapping subproblems before promising a speedup.
  • The recursion is memoized but still too slow. What do you look at first?
    The state definition. An over-specified state — carrying data the recurrence does not actually need — multiplies the state count and destroys the overlap that makes caching work. Second, the transition: a per-state scan may be replaceable with a running aggregate. Third, whether the cache key itself is expensive to build or compare.

Working out a hard sum once and writing it on a sticky note: the total effort is how many different sticky notes you end up needing, times what each one costs to work out — not how many times someone asks you for one.

saying these in an interview costs you the question

  • Says memoization always makes it linear
  • Quotes a complexity without naming the state space
  • Counts cache entries but ignores per-state work
  • Assumes subproblems overlap without checking
  • Treats the cache's memory as free
  • Counts nodes of the recursion tree as distinct states

context

open as a page

Memoized recursion vs a bottom-up DP table: what actually differs between them?

level: juniorimportance: must knowfreq 76%

basics

~20 s

Both evaluate the same recurrence at the same asymptotic cost. Memoization recurses from the goal downward, caching answers on demand and touching only states it can reach. Tabulation fills every state in an order you choose, using no call stack.

open as a page

Why can a bottom-up DP that fills a 2D table often keep only two rows in memory?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Only the cells that a pending transition still reads have to stay in memory. When every value in row i is computed from row i-1 alone, the finished earlier rows are dead weight, so two rows are enough.

open as a page

What should dp[0] hold in a bottom-up DP table indexed by items considered so far?

level: juniorimportance: must knowfreq 68%

basics

~20 s

dp[0] is the empty-prefix answer: zero items considered, not the first item. Seed it with the identity of whatever the transition combines, such as 0 for a sum, 1 for counting the single empty arrangement, or a large sentinel for a minimisation. The first item's result lands in dp[1].

open as a page

Why is a DP that fills an n x K table with an inner scan over earlier positions not O(n*K)?

level: middleimportance: must knowfreq 72%

basics

~20 s

DP time is (number of states) times (cost of one transition), not table size alone. With nK cells and an inner scan of up to n predecessors, the cost is O(n^2K); counting cells hides the inner loop.

open as a page

When a 2D DP is collapsed to one array, why does the loop direction become a correctness issue?

level: middleimportance: must knowfreq 58%

basics

~20 s

Direction decides whether a read still sees the previous pass's value. In one array a write can clobber a cell the same pass later reads, so the wrong order silently computes a different recurrence — a wrong answer, not a slow one.

open as a page

A battery-dispatch DP indexed only by interval gives wrong answers — which state dimension is missing?

level: middleimportance: must knowfreq 72%

basics

~20 s

The state forgets whether the battery currently holds a charge and whether it is inside the mandatory idle window. Add that dimension — one cell per interval per situation (empty, holding, cooling) — because two plans reaching the same interval have different legal futures and must not share a cell.

open as a page

For an n x W DP table compressed to a single row, what are the space and time complexities?

level: middleimportance: should knowfreq 55%

basics

~10 s

Space drops from O(nW) to O(W) because only one row is kept, while time stays O(nW): compression removes stored cells, not computed ones. A recursive formulation's stack depth counts toward space as well.

open as a page

Converting a memoized recursion to a bottom-up table: how do you derive the fill order?

level: middleimportance: should knowfreq 55%

basics

~20 s

Read off which states each recursive call depends on, then fill the table in any order where every dependency is already final when a cell is written — usually the reverse of the direction the recursion moves. Base cases become cells seeded before the loop.

open as a page

In a 2D DP table filled row by row, why does a transition that reads dp[i][j+1] fail silently?

level: middleimportance: should knowfreq 50%

basics

~20 s

Row-major filling writes left to right, so dp[i][j+1] has not been computed yet — it still holds the table's seed value. The transition folds that seed into a max or a min as if it were a real result, producing a wrong answer with no crash and no warning.

open as a page

A memoized recursion over 200k log events overflows the stack in production — what went wrong?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The cache prevents repeated work, not depth. The first descent to a base case is one unbroken chain of pending calls as long as the event list, so recursion depth grows with the input while the state count stays modest. Small test inputs never reach the limit.

open as a page

A batch job runs out of memory filling a DP table over two 100,000-element sequences — what do you change?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Ten billion cells is the problem, not the allocator. Keep only the rows the transition still reads — two rows is 200,000 cells — and the job fits. That fixes memory alone; ten billion cell computations still cost the same time.

open as a page

Why is dp[i][timeA][timeB] one dimension too many when splitting jobs between two crews?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Each of the first i jobs goes to exactly one crew, so timeB always equals the prefix total minus timeA. The third component is derived, not free information: it multiplies states and work by the whole time range while every cell that violates the identity is unreachable. Track timeA and compute timeB.

open as a page

A DP over crates must report the chosen set, not just the optimum — how do you budget memory for that?

level: principalimportance: should knowfreq 36%

basics

~20 s

Rolling the table away destroys the record of which choice each cell made, so reconstruction needs something kept: the full table, one decision bit per cell, parent pointers, or a divide-and-conquer scheme that recomputes halves to trade time for space.

open as a page

A subset-sum DP runs in O(n*W) with amounts up to 10^9 cents — why isn't that polynomial time?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

Complexity is measured against input length in bits, and the bound W needs only about log W bits to write down. O(n*W) is therefore exponential in the encoding length — pseudo-polynomial, which is why subset-sum stays NP-hard despite the table.

open as a page

When only a fraction of a DP's table cells are reachable, does memoization or tabulation win?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Memoization wins on a sparse state space: it computes only states actually reached from the goal, while a table pays time and memory for every declared cell. A scheduling DP with ten million cells but ten thousand reachable states is three orders of magnitude of wasted work for the table.

open as a page