bigonotes
DSA: Patterns & PythonDynamic Programming

DP Overview & State Design

How to spot a DP problem, name its pattern from the wording, and design a state whose cache key captures everything.

7 min readLast updated

Dynamic programming is recursion that remembers. When a brute-force search keeps asking the same smaller question ("best answer from index i with cap left"), you store each answer once and reuse it. Most DP problems fall into a handful of patterns, and the wording of the problem usually tells you which one.

The idea#

A DP solution is a function of a state: the smallest set of values that fully decides the answer from this point on. You write it in four steps.

  1. State: what changes between subproblems (an index, remaining capacity, a pair of indices, a flag).
  2. Transitions: the choices at this state (take or skip, move down or right, match or delete).
  3. Base cases: where the recursion stops (ran out of items, crossed pointers, hit the target).
  4. Answer: which state you call first, and how you combine the choices (min, max, sum, or).

Those four steps pay off only when the problem has two properties: overlapping subproblems (the same state is reached by many paths) and optimal substructure (the best answer from a state only depends on that state, not on how you got there).

Pattern map#

Once you can write a state, the next question is which shape to reach for. Match the clue in the problem to a pattern below; each pattern has its own note.

Clue in the problem Pattern Note
"previous element matters", can't take neighbors Linear 1-D House Robber family
fewest pieces to make exactly n 1-D min/max to a target Min/Max to a Target
"take or skip each item", each used once 0/1 knapsack 0/1 Knapsack
"unlimited supply", count the ways Unbounded knapsack Combinations vs Permutations
grid, top-left to bottom-right, k moves Grid / path Grid & Path DP
two strings or arrays to compare Two-string Two-String DP
palindromes, "split into parts", burst/cut Interval Palindrome & Interval DP
"subsequence order", nesting, chains LIS LIS Family
buy/sell/cooldown, a mode you are in State machine State-Machine DP
two players play optimally Game / minimax Game / Minimax DP

Two close relatives live elsewhere. Tree DP (return several states from a postorder call, as in House Robber III) is in Tree DP with States. Kadane's "extend or restart" running state is in Kadane & Running-State DP.

Memo, then table, then space#

Whichever pattern you land on, the code goes through the same three stages. Start top-down with @cache: it's the brute-force recursion plus one decorator. Once it's correct, you can turn it into a bottom-up table that fills states in dependency order. Then keep only the rows the recurrence reads. Here are the three shapes for a recurrence that reads the last two states.

top-down:
    f(i):
        if i is a base case: return its value
        return combine(f(i - 1), f(i - 2))
    memoize f on i; answer f(n)

table:
    dp[base cases] = their values
    for i from the first non-base state up to n:
        dp[i] = combine(dp[i - 1], dp[i - 2])
    answer dp[n]

rolling:
    prev2, prev1 = the two base values  # only two cells are read
    repeat up to n:
        prev2, prev1 = prev1, combine(prev1, prev2)
    answer prev1

The memo version is usually enough in an interview. Mention the table and the rolling version as follow-ups. Move to them when recursion depth is a risk (Python's default limit is about 1000 frames).

Designing the state#

The state is where most DP bugs and slow solutions come from. These four checks catch the usual problems.

Everything the answer depends on must be in the cache key#

@cache only sees the function's arguments. Suppose the answer also depends on something outside them, such as a shared used[] list that the recursion flips on and off. The cache then returns answers computed under a different used[]. The result is wrong, or the code only looks memoized and is still exponential.

Symptom Fix
Recursion reads or mutates an outer list, set or counter Move that information into the arguments, or redesign so it isn't needed
An argument is only recursion depth (it never changes the answer) Drop it and add the argument that does matter
"Pick any unused item" loop inside the recursion Process items in a fixed order and decide take or skip for item i

This trap shows up most in "choose some of these items" problems written as "loop over every unused item". Each one has a clean state once you fix the order; see the hints below.

Shrink the state when a dimension is redundant#

Even a correct key can carry more than it needs. Ask whether the answer from here depends on how you got here. If order doesn't matter for the answer, the history can usually go, and only "what is left" stays in the state. See Min/Max to a Target.

Flip the question when the obvious state is too big#

Sometimes "the fewest moves to do X" has a huge state, or a slow search inside each state. Flip what the DP returns: "with these resources and m moves, how much can you do?" Then find the smallest m that is enough.

Reduce to a pattern you already know#

Many problems are a known pattern in disguise. Before inventing a new recurrence, ask which pattern the choices look like. The pattern map above is the list to check.

Worth exploring: digit DP (count numbers up to N with a property, using a "tight" flag per digit, e.g. LC 902, 1012) and bitmask DP (dp[mask] over subsets, e.g. LC 691, 847, 1125). Both build on the state-design ideas above.

Pitfalls#

Most DP bugs are quiet: the code runs but returns a wrong answer or stays slow. Watch for these.

  • A cache key that misses part of the state (see above). This is the most common silent bug.
  • Memoizing on string slices (track(word1[:-1], word2)). Each key costs O(n)O(n) to build and hash; pass indices instead.
  • Writing base cases (such as off-the-grid positions) into the memo. It's harmless but wastes memory; return them directly and cache only real states.
  • Off-by-one guards such as if rem < -1 where rem < 0 was meant. A negative remainder must be rejected.
  • Taking a modulus only at the end. In Python big integers hide the problem, but the intended solution applies % MOD at every step.

Problems in this pattern#

These problems use this pattern. Solve them before you open the hints below. Each one tests a state-design move from this note.

Hints#

Open a hint only after you have tried the problem for a while.

Hint: Climbing Stairs

ways(i) = ways(i - 1) + ways(i - 2), and steps 0 and 1 have one way each. Write all three shapes: memo, table, then two rolling variables.

Hint: Ones and Zeroes

Don't loop over every unused string. Decide take or skip for string i in order, with the state (i, zeros_left, ones_left).

Hint: Maximum Length of Pair Chain

A "try every unused pair" recursion breaks the cache. Sort the pairs, then it is an LIS problem.

Hint: Video Stitching

The "pick any unused clip" recursion has the same cache trap. A greedy reach works better: extend as far as possible from the current covered end.

Hint: Perfect Squares

The state (largest square allowed, total so far) works but is 2-D. Ask "how many squares do you need to reach exactly rem?" The order of the squares doesn't matter for a count, so rem alone is the state.

Hint: Super Egg Drop

A state of (eggs, floors) with a search over the drop floor is slow. Flip it: with k eggs and m moves, how many floors can you cover? That gives cover(m, k) = cover(m-1, k-1) + cover(m-1, k) + 1. Find the smallest m that covers n floors.

Hint: Delete and Earn

Taking a value blocks its neighboring values. That is House Robber over values.

Hint: Last Stone Weight II

The smashing story hides a min-difference partition. See 0/1 Knapsack.

Hint: Delete Operation for Two Strings

Keep the longest common subsequence and delete the rest: len1 + len2 - 2 * LCS.

Key takeaways#

  • Write the state, transitions, base cases and answer before writing code.
  • The cache key must hold everything the answer depends on; nothing global may leak in.
  • Start with @cache, then convert to a table and roll it down to the rows you need.
  • Match the problem's wording to a pattern from the map, and look for a reduction to a known one.
  • If the state feels too big, drop a redundant dimension or flip what the DP returns.
Revision notes · 2 min

Core idea#

Dynamic programming is recursion that remembers. You store the answer for each state once and reuse it when another path reaches the same state.

Key points#

  • Write four things before code: state, transitions, base cases and answer.
  • The state is the smallest set of values that fully decides the answer from this point on.
  • DP needs overlapping subproblems and optimal substructure.
  • Start top-down with @cache. Then convert to a bottom-up table, and keep only the rows the recurrence reads.
  • Move to the table when recursion depth is a risk. Python's default limit is about 1000 frames.
  • The cache key must hold everything the answer depends on. A shared outer used[] list breaks the cache.
  • Replace "pick any unused item" loops with a fixed order and a take-or-skip choice for item i.

Pattern map#

Clue in the problem Pattern
Can't take neighbors Linear 1-D (House Robber)
Fewest pieces to make exactly n Min/max to a target
Take or skip, each item once 0/1 knapsack
Unlimited supply, count the ways Unbounded knapsack
Grid, top-left to bottom-right Grid / path
Two strings or arrays Two-string
Palindromes, split, burst or cut Interval
Subsequence order, nesting, chains LIS
Buy/sell/cooldown, a mode State machine
Two players play optimally Game / minimax

State design moves#

  • Shrink: drop a redundant dimension. Perfect Squares needs only rem, not (largest square, total).
  • Flip: change what the DP returns. Super Egg Drop asks how many floors k eggs and m moves cover.
  • Reduce: Delete and Earn is House Robber over values. Delete Operation for Two Strings is len1 + len2 - 2 * LCS.

Pitfalls#

  • A cache key that misses part of the state. This is the most common silent bug.
  • Memoizing on string slices. Each key costs O(n)O(n) to build and hash. Pass indices.
  • Guards like rem < -1 where rem < 0 was meant.
  • Taking the modulus only at the end. Apply % MOD at every step.

Say it in an interview#

  • "The state is ..., the choices are ..., and the base case is ..."
  • "Memoized recursion first. Then a table, rolled down to the rows the recurrence reads."

Found a mistake? Email ask@bigonotes.com