// pattern debugger≡ menu

stack>advanced patterns / dp

// Dynamic Programming Basics

State + recurrence + base case. 1D, 2D, and string DP through the six problems that teach the whole method.

core idea

Every DP problem is four decisions, made in order: what’s the state (the minimal set of parameters that pins down a subproblem), what’s the recurrence (how a state’s answer is built from smaller states’ answers), what’s the base case (the states you can answer without recursing), and what’s the order (fill states so every dependency is already known when you need it). Two ways to run the same recurrence once you’ve named all four:

approach direction mechanics
memoization (top-down) recursion, cached write the brute-force recursion first, add a cache keyed by state — done
tabulation (bottom-up) iteration, filled table flip the recursion into a loop that fills states in dependency order

DP is backtracking with a memory

Strip the cache out of any DP solution and what’s left is a plain backtracking recursion — same states, same choices, same base case. What turns exponential backtracking into polynomial DP is that the state space is small and the same states get asked for repeatedly: backtracking explores a tree where most branches are genuinely distinct; DP recognizes the tree secretly has repeated nodes and answers each one once.

when to reach for it

  • The problem asks to count the ways, or find a minimum/maximum cost or value, over a sequence of choices.
  • A brute-force recursion would work but is exponential — and tracing it shows the same arguments being recomputed over and over.
  • The problem has optimal substructure: the best answer for the whole input is built from the best answers to smaller versions of the same problem, not just any answer to them.
  • You can name the state in a sentence (“the best score up to index i”, “the fewest coins to make amount a”) and that sentence has few enough distinct fillings to be practical — usually a polynomial count.

universal templates

Top-down (memoization) — write the recursion you’d write anyway, then cache it:

public int TopDown(int state, Dictionary<int, int> memo)
{
    if (IsBaseCase(state)) return BaseValue(state);
    if (memo.TryGetValue(state, out var cached)) return cached;

    int result = Recurrence(state);    // combines TopDown(smaller states)
    memo[state] = result;
    return result;
}

Bottom-up (tabulation) — the same recurrence, run forward through every state in dependency order:

public int BottomUp(int n)
{
    var dp = new int[n + 1];
    dp[0] = BaseValue;                 // seed the base case(s)

    for (int i = 1; i <= n; i++)
        dp[i] = Recurrence(dp, i);     // only reads already-filled entries

    return dp[n];
}

Every problem below names its state, writes the recurrence, and picks a direction. Most people think top-down first and ship bottom-up — the array is usually smaller than the recursion’s call stack, and there’s no depth limit to worry about.

rolling arrays

If dp[i] only ever reads dp[i-1] (and maybe dp[i-2]), you don’t need the array at all — two or three variables suffice, and space drops from O(n) to O(1). Climbing Stairs and House Robber both compress this way; Unique Paths shows the same trick one dimension up, rolling a 2D table down to a single row.

problems

  1. 01Climbing StairseasyLC #70

    Fibonacci in disguise — the "ways to reach i" recurrence.

  2. 02House RobbermediumLC #198

    Take-or-skip: dp[i] = max(dp[i-1], dp[i-2] + a[i]).

  3. 03Unique PathsmediumLC #62

    2D grid DP: cell = sum of the two ways in; rows compress to 1D.

  4. 04Coin ChangemediumLC #322

    Min-coins per amount, built bottom-up — the unbounded knapsack shape.

  5. 05Word BreakmediumLC #139

    DP over string prefixes, with a word set doing the lookups.

  6. O(n²) DP first; the O(n log n) patience upgrade as an optional coda.

cheat sheet — dp

recognize it

  • "count the ways" / "minimum or maximum cost or value" over a sequence of choices → DP
  • a brute-force recursion re-derives the exact same arguments over and over (ways(n-2) from two branches, dp[a-5] feeding multiple later as) → overlapping subproblems
  • optimal substructure: the best answer for the whole input is built from the best answers to strictly smaller versions of the same problem, not just any answer to them
  • a grid with only right/down moves, or a string question about splitting or matching → 2D or string DP — the family that also includes LCS (1143) and Edit Distance

key tricks

  • name the state as a sentence first (dp[i] = ...) — the recurrence falls out once the sentence is precise
  • write the brute-force recursion, memoize it top-down to prove it's right, then flip to a bottom-up loop once it works
  • if dp[i] only ever reads dp[i-1]/dp[i-2], roll the table down to 2-3 variables — O(n) space becomes O(1)
  • Kadane (Maximum Subarray) is DP already compressed to one variable — recognize it before reaching for a fancier tool
  • fixed predecessors (i-1, i-2, Climbing Stairs / House Robber) vs. variable predecessors (every coin, every dictionary word, every j < i) is the fork between the two solution shapes

common bugs

  • wrong loop order — reading a dp entry before it's filled, or accidentally computing the "combinations" variant instead of "minimum" by swapping which loop is outermost (Coin Change vs. Coin Change II)
  • off-by-one on the base case — indexing dp[i-1]/dp[i-2] before the array holds anything real
  • an "impossible" sentinel that can look like a real answer, or that overflows once you add 1 to it (int.MaxValue + 1)
  • returning dp[n-1] when the true answer is max(dp) over every state — the optimum doesn't always end at the last index (LIS)

// connections

  • Array & Matrix Techniques — Kadane is the DP you already know — one-variable table
  • Backtracking — DP is memoized backtracking when subproblems overlap
  • Greedy — Jump Game solves both ways — learn which proof each needs