Phase 5 — Dynamic Programming (Basic → Extreme)

Target level: Medium → Very Hard Expected duration: 4 weeks (12-week track) / 5 weeks (6-month track) / 6 weeks (12-month track) Weekly cadence: ~6 DP topics per week + 30–60 problems applying them under the framework


Why Dynamic Programming Is The Single Hardest Pattern Family In Coding Interviews

Phase 4 taught you that one-in-three Medium-Hard interview problems is a graph problem in disguise. The other big share is dynamic programming. DP shows up in roughly one in four Medium-Hard rounds at top-tier companies, and the share rises further in staff/principal and quant interviews where exact-counting and optimization questions dominate. More importantly, DP is the topic where the gap between candidates who have a framework and those who don’t is widest. A candidate without a DP framework freezes on dp[i][j] = ?; a candidate with one writes the recurrence in 90 seconds and spends the remaining time on edge cases.

The empirical claim that drives this entire phase:

The hard part of DP is not the code. The hard part is deriving the state. Almost every wrong DP solution is wrong because the state is wrong — too small to capture all the information needed, or so large that the table doesn’t fit in memory or time. Once the state is right, the transition writes itself, the base cases follow from the state, and the code is mechanical.

DP is also the topic where candidates most often memorize problems instead of internalizing the technique, and it is the topic where memorization fails most spectacularly. There are perhaps 60 named DP problems on LeetCode that everyone has seen; an interviewer who wants to filter out the memorizers asks the 61st. The solution, then, is not to drill 60 problems — it is to drill the derivation process until you can derive any DP from the recursive formulation.

This phase is built around one teaching device that we will use on every single problem from start to finish: the brute → memo → tabulated → space-optimized progression. Every problem you solve in this phase will be solved four times in succession:

  1. Brute force — usually exponential recursion that explores every choice.
  2. Memoized — the same recursion plus a cache, top-down DP, O(states × transition) time.
  3. Tabulated — bottom-up loop in topological order on the state DAG, O(states × transition) time, no recursion stack.
  4. Space-optimized — a rolling-array transformation that keeps only the previous one or two layers and reduces space from O(N · M) to O(M) or O(1).

By the end of this phase, you will execute this progression unconsciously. When an interviewer asks “can you reduce the space?”, you will already have written the tabulated version with the rolling array in mind. When the interviewer asks “what’s the recurrence?”, you will already have derived it from the brute-force recursion in the first 90 seconds of the problem. The four-stage progression is the single most valuable interview-time discipline taught in this entire curriculum, because it converts an open-ended “design a DP” question into a deterministic four-step recipe.

After this phase, you can solve canonical Hard DP problems on first attempt: edit distance in 25 minutes with full progression, longest increasing subsequence at both O(N²) and O(N log N), partition equal subset sum, coin change (count and minimum), burst balloons (interval DP), house robber III (tree DP), and shortest path visiting all nodes (bitmask DP). You will also become visibly stronger in mock interviews because you will reach for dp[i][j] notation on the whiteboard within 90 seconds and articulate the state definition out loud before writing any code.


What You Will Be Able To Do After This Phase

  • Recognize that a problem is a DP problem within 2 minutes, even when the words “DP” or “memoize” never appear in the statement.
  • Derive the state from a recursive brute force in <3 minutes by identifying the parameters that change across recursive calls.
  • Write the transition as a closed-form max/min/sum over a small set of choices, with clear correctness justification.
  • Identify base cases as the recursive function’s return statements at the smallest input.
  • Write the tabulated version by inverting the recursion into a loop with the right evaluation order on the state DAG.
  • Apply the rolling-array trick to reduce O(N · M) space to O(M) or O(1) when the recurrence depends only on the previous row(s).
  • Distinguish 0/1 knapsack from unbounded knapsack by a single change in the inner-loop direction.
  • Recognize when a “subset” or “partition” problem reduces to subset sum with target total / 2.
  • Implement LIS at both O(N²) (canonical DP) and O(N log N) (patience sorting + binary search) and explain the equivalence.
  • Implement edit distance with all four variants (brute, memo, tabulated, O(M)-space) in <25 minutes.
  • Implement tree DP via post-order recursion, returning multi-tuple state (e.g., “best with root included” and “best with root excluded”).
  • Implement interval DP with the canonical for length, for left, right = left + length - 1 loop structure.
  • Implement bitmask DP with state (mask, last) for TSP-style problems and (mask) alone for set-cover-style problems.
  • Articulate the correctness argument for every DP you write: state definition, transition justification, evaluation order, base case.
  • Spot the standard DP bugs unprompted: wrong base case, wrong evaluation order, off-by-one in indices, missed edge case at empty input.

How To Read This Phase

Read this README in two passes. Pass 1: linear, end-to-end, building a mental map of which DP variant solves which problem signal. Do this in one sitting. Pass 2: as you work the labs, refer back to specific topic entries to clarify state-design choices and pitfalls.

Each topic entry has a fixed shape:

  1. When To Use — the problem signal that should fire this DP variant in <2 minutes.
  2. State Design — what the state is, why these are the right parameters, why no fewer suffice.
  3. Transition — the recurrence in closed form.
  4. Complexity — time and space, and what space optimization is possible.
  5. Common Pitfalls — the bugs that consume the most interview minutes for this DP variant specifically.
  6. Classic Problems — 3–5 representative LeetCode problems where this DP is the intended solution.

The phase ends with a DP-Recognition Cheat Sheet (problem signals → DP variant), a Common-Bug Catalog, a Mastery Checklist, and Exit Criteria.


The DP Framework

Before any topic, internalize this framework. Use it on every DP problem.

1. State Definition

The state is the smallest set of parameters that uniquely determines the answer to a subproblem. Write it down explicitly:

dp[i] = the answer to the subproblem ending at / using up to / for prefix-of-length / etc., parameter i.

Sentences that begin “let dp[i] be” are the most valuable two seconds of the entire problem. If you can’t finish the sentence, you don’t have a state — you have a vague hope.

The state must be sufficient (encodes everything the future needs to know) and necessary (every parameter actually changes the answer). A common bug is a state that’s sufficient but not necessary — e.g., tracking both index and remaining-budget when budget is determined by index. Another is necessary but not sufficient — e.g., tracking only the index when the choice depends on what was picked earlier.

A useful test: two equal states must produce equal answers. If two different histories arrive at the same state but have different optimal continuations, the state is missing a dimension.

2. Transition Function

The transition expresses dp[state] as a function of dp[smaller_state] for one or more smaller states. It is the recurrence. For optimization problems, it is min or max over a small set of choices; for counting problems, it is a sum.

The transition has three parts:

  • Choices — the discrete set of moves at this state (include item or skip; pick this character or that one; rob this house or skip).
  • Cost / value — the contribution of each choice (the item’s weight, the operation’s cost, the gain from picking).
  • Aggregationmin / max / sum / OR over the choices.

Write the transition as:

dp[state] = aggregate over choice c in C(state): contribution(c) + dp[state - effect(c)]

Always keep C(state) finite and small — typically O(1), O(K), or O(N). Transitions that aren’t O(small) usually indicate a missing state dimension.

3. Base Cases

The base cases are the values of dp at the smallest (recursion-stopping) states. They are not optional; a missing or wrong base case is the single most common DP bug.

Identify base cases by writing the recursion first and looking at the return-statements:

def f(i):
    if i == 0: return 0  # ← THIS is the base case
    return f(i-1) + something

For 2D DP, the base cases are typically the entire first row and first column — set them explicitly before the main loop. For 3D DP and beyond, they’re a hyper-plane of dimension one less than the state.

A subtle base case bug: two different recursive paths reach the same base case but expect different return values. Usually this means the state is wrong (missing a dimension) and the base case has to “remember” which path it came from — impossible.

4. Evaluation Order

DP states form a DAG: state A points to state B iff dp[B] appears in the recurrence for dp[A]. To compute dp[A] we must have already computed dp[B]. The evaluation order is a topological order of this DAG.

For 1D DP indexed by i, the order is usually i = 0, 1, 2, …, N (increasing) or i = N, N-1, …, 0 (decreasing) — depending on whether your transition looks “back” or “forward”. Both work; pick one consistently.

For 2D DP indexed by (i, j), the order is usually row-major (for i: for j:) or column-major. The right one is the one that fills dp[i-1][j] and dp[i][j-1] before dp[i][j].

For interval DP indexed by (left, right), the order is by interval length ascending: for length in 1..N: for left, right = left + length - 1. This guarantees all sub-intervals are filled before the enclosing interval.

For tree DP, the order is post-order DFS: children are filled before the parent.

For bitmask DP, the order is by popcount ascending or by mask value ascending (since a sub-mask of m is < m).

5. Space Optimization (The Rolling-Array Technique)

If dp[i][j] depends only on dp[i-1][*] and dp[i][j-1], then once row i is computed we don’t need any earlier row. Keep only the current and previous row — O(N · M) becomes O(M).

If dp[i][j] depends only on dp[i-1][j] and dp[i-1][j-1] (no same-row dependency), you can collapse to a single row using right-to-left iteration in j — O(M) space.

If dp[i][j] depends on more rows (e.g., dp[i-2]), keep that many rows.

The rolling-array transformation is mechanical once the tabulated version is written. The interviewer often asks for it: “can you reduce the space?” Practice the transformation on every lab so it becomes reflex.

6. The Brute → Memo → Tabulated → Space-Optimized Progression

Apply this on every problem in this phase. It is the mandatory teaching device of this phase.

  1. Brute force: exponential recursion that tries every choice. Often O(2^N) or O(N!). Don’t skip writing this — it is the direct source of your state and transition.
  2. Memoized: same recursion + a cache (@lru_cache in Python, a HashMap in Java, an array in C++). Time becomes O(states × transition); space includes the recursion stack.
  3. Tabulated: replace recursion with a loop in topological order on the state DAG. Same time complexity, but no recursion overhead, and the loop structure makes the dependency pattern explicit.
  4. Space-optimized: roll the table down to O(M) or O(1) by exploiting the recurrence’s locality.

Each stage is a strictly smaller change from the previous: brute → memo is “add a cache”, memo → tabulated is “invert the call graph”, tabulated → space-optimized is “drop dimensions you don’t reuse”. This is deterministic engineering, not invention.


Inline DP Topic Reference


1. Memoization Vs Tabulation Tradeoffs

When To Use

Both compute the same thing — they’re two evaluation orders on the same state DAG. Choose deliberately.

  • Memoization (top-down): when the reachable set of states is much smaller than the full state space (sparse DP). Examples: regex matching where many (i, j) pairs are never visited. Also when the transition is easier to express recursively than as a forward loop.
  • Tabulation (bottom-up): when the state space is dense (most states are visited), when you need to reduce space via rolling arrays (which require explicit loop structure), or when recursion depth would exceed the stack limit (e.g., N=10^5 in Python with default setrecursionlimit=1000).

Common Pitfalls

  • Memoization in Python with default setrecursionlimit overflows at N ≈ 1000. Either sys.setrecursionlimit(10**6) or convert to tabulation.
  • Memoization with mutable arguments (e.g., lru_cache on a function taking a list) — Python lru_cache requires hashable arguments; pass tuples or indices, not lists.
  • Tabulation with the wrong loop order silently produces garbage — see Section 5.

Classic Problems

  • LC 322 — Coin Change. Memoized recursion is natural; tabulated is faster in tight loops. See Lab 04.
  • LC 10 — Regular Expression Matching. Memoization is cleaner here. See String DP below.

2. State Design Principles

When To Use

Every DP problem starts here. Follow this discipline:

  1. Identify what changes across recursive calls. Those parameters are candidate state dimensions.
  2. Drop any parameter that is determined by the others.
  3. Keep any parameter whose value affects the optimal continuation.
  4. Verify: two states with identical parameters must have identical optimal values.

State Design Patterns

  • Prefix DP: dp[i] = answer for first i elements. Used in LIS, house robber, decode ways.
  • Two-pointer / interval DP: dp[i][j] = answer for elements in [i, j]. Used in matrix chain, burst balloons, palindromic subsequence.
  • Knapsack-style: dp[i][w] = best using first i items with budget w. Used in 0/1 knapsack, partition, coin change.
  • Two-string DP: dp[i][j] = answer for prefix-i of A and prefix-j of B. Used in LCS, edit distance, regex matching.
  • Tree DP: dp[v] = answer for subtree rooted at v. Often a tuple of values (e.g., “rob” / “skip”).
  • Bitmask DP: dp[mask] or dp[mask][last] = answer over the subset specified by mask.
  • Game DP: dp[state] = best score the current player can guarantee.

Common Pitfalls

  • Adding a parameter that doesn’t affect the answer — wastes time and space. E.g., tracking step_count when the recurrence already encodes it via the index.
  • Missing a parameter that does — produces wrong answers because two materially different histories collapse to the same dp cell.
  • Encoding choices in the state instead of the transition. The state is “where are we now”; the transition decides “what to do next”. Keep them separate.

3. Transition Function Design

When To Use

Once the state is defined, the transition is constrained: it must express dp[state] in terms of strictly smaller states.

Design Steps

  1. List all choices available at this state (include / skip / pick which / move where).
  2. For each choice, identify the contribution and the resulting smaller state.
  3. Aggregate: min for shortest/cheapest, max for longest/most-valuable, + for counting.

Common Pitfalls

  • Forgetting a choice — usually “don’t take the item” or “skip this position”. Often the trivial choice that the recurrence still depends on.
  • Double-counting — particularly in counting problems where two distinct paths to the same state are aggregated naively. Often signals a missing dimension.
  • Off-by-one in the resulting smaller statedp[i-1][j-1] vs dp[i][j-1] is the difference between “use this character” and “use the prefix ending here”.

4. Base Case Identification

When To Use

After defining state and transition, the recursion bottoms out at some smallest state. The base case is what dp returns there.

Identifying Base Cases

  • For prefix DP dp[i]: the base case is dp[0] — the empty prefix. Its value is the natural identity (0 for sums, 1 for counts of “the empty product”, -∞ or +∞ for unreachable).
  • For interval DP dp[i][j]: the base case is dp[i][i] (length-1 interval) — its value depends on the problem.
  • For two-string DP dp[i][j]: the base cases are dp[0][j] = j and dp[i][0] = i for edit distance, or dp[0][j] = dp[i][0] = 0 for LCS.

Common Pitfalls

  • Wrong identity for counting problems — the empty prefix has count 1 (one way to make nothing), not 0.
  • Wrong identity for min / max — initialize to +∞ / -∞, not 0. Initializing to 0 silently makes “do nothing” look optimal.
  • Forgetting to set base cases on the boundary of a 2D table — leaves them as the language’s default (0 in Java arrays; null in JS; uninitialized garbage in C).

5. Evaluation Order (Topological Order On The State DAG)

When To Use

Every DP. The evaluation order must be consistent with the dependency structure.

Determining The Order

Treat states as nodes; draw an edge from A to B iff the recurrence for A reads dp[B]. The order to evaluate is reverse topological (children before parents).

For most DPs the order is obvious: increasing i, increasing j, increasing interval length, post-order over the tree, increasing popcount of mask. When in doubt, fix a small example, write out the dependency arrows, and read off the order.

Common Pitfalls

  • 2D DP iterated in the wrong order silently computes garbage. The classic bug: iterating for j: for i: when the recurrence reads dp[i-1][j] and dp[i][j-1]. The latter is fine only if the inner loop fills the column top-to-bottom and you compute it in the right order.
  • Interval DP iterated in (left, right) order instead of (length, left) — fails because you compute dp[0][N-1] before dp[1][N-1].
  • Bitmask DP iterated by some-arbitrary-order instead of mask value ascending — fails if any sub-mask is read after the enclosing mask is written.

6. Space Optimization (Rolling Array Technique)

When To Use

Whenever dp[i][...] depends on only the previous one or two i values, you can keep just those rows.

Mechanical Transformation

  • Replace dp[i][j] with dp_curr[j] and dp[i-1][j] with dp_prev[j].
  • After each i, swap or copy.
  • If dp[i][j] doesn’t depend on dp[i][k] for k < j (no same-row dependency), collapse further to a single 1D dp[j]. Iterate j carefully: if the recurrence reads dp[i-1][j-1], iterate j from right-to-left so you read the old value before overwriting.

Common Pitfalls

  • Iterating left-to-right when right-to-left is needed — overwrites the value you’ll need next. This is the canonical 0/1 knapsack vs unbounded knapsack distinction:
    • 0/1 knapsack: iterate weight right-to-left to use the previous-row’s dp[w-wi].
    • Unbounded knapsack: iterate weight left-to-right to use the current row’s dp[w-wi] (because items can be reused).
  • Forgetting to reset the rolling array between outer iterations — old values bleed through.
  • Optimizing space prematurely, before the tabulated version is correct. Always verify tabulated against memoized on small inputs first.

7. 1D DP

When To Use

The state is a single integer index — dp[i]. Examples: climbing stairs, house robber, decode ways, max subarray (Kadane).

State Design

dp[i] = answer for the prefix ending at index i, OR for the first i elements. Pick one convention and stick with it (the lab uses “answer for first i elements” consistently).

Transition

dp[i] = f(dp[i-1], dp[i-2], ..., dp[i-k]) for some small k. Examples:

  • House robber: dp[i] = max(dp[i-1], dp[i-2] + house[i-1]).
  • Climbing stairs: dp[i] = dp[i-1] + dp[i-2] (Fibonacci).
  • Decode ways: dp[i] = (dp[i-1] if s[i-1] is valid 1-digit) + (dp[i-2] if s[i-2:i] is valid 2-digit).

Complexity

Time O(N). Space O(N) tabulated, O(1) space-optimized (since dependence on at most O(1) previous values).

Common Pitfalls

  • Off-by-one between dp[i] and house[i] — confusion between “first i houses” (uses house[i-1] as the latest) and “ending at index i” (uses house[i]). Pick one and never mix.
  • Forgetting the empty casedp[0] for “first 0 elements” must be the identity.

Classic Problems

  • LC 70 — Climbing Stairs. See Lab 01.
  • LC 198 — House Robber.
  • LC 91 — Decode Ways.
  • LC 53 — Maximum Subarray (Kadane).
  • LC 746 — Min Cost Climbing Stairs.

8. 2D DP

When To Use

The state is a pair of integers — dp[i][j]. Examples: unique paths on a grid, minimum path sum, longest common subsequence, edit distance.

State Design

For grid problems, dp[i][j] = answer for getting to cell (i, j). For two-string problems, dp[i][j] = answer for prefix-i of one string and prefix-j of the other.

Transition

For grid: dp[i][j] = dp[i-1][j] + dp[i][j-1] (count of paths) or min(dp[i-1][j], dp[i][j-1]) + grid[i][j] (min path sum).

Complexity

Time O(N · M). Space O(N · M) tabulated, O(M) with rolling rows, O(M) with right-to-left collapse to 1D when there’s no same-row dependency.

Common Pitfalls

  • Initializing first row and first column wrong for grid path problems — these are not always 0 or 1; they may carry obstacles or grid values.
  • Adding grid[i][j] to all transitions including the boundary — the boundary needs special handling.

Classic Problems

  • LC 62 — Unique Paths.
  • LC 63 — Unique Paths II (with obstacles). See Lab 02.
  • LC 64 — Minimum Path Sum.
  • LC 120 — Triangle.

9. 0/1 Knapsack

When To Use

A set of N items each with weight w_i and value v_i; capacity W; maximize value subject to total weight ≤ W. Each item used at most once. Recognized by discrete choices over a budget.

State Design

dp[i][w] = max value using first i items with capacity w.

Transition

dp[i][w] = max(dp[i-1][w], dp[i-1][w - w_i] + v_i) if w >= w_i, else dp[i-1][w]. The two cases: skip item i, or take it.

Complexity

Time O(N · W). Space O(W) with right-to-left collapse: iterate w from W down to w_i.

Common Pitfalls

  • Iterating w left-to-right in the 1D-collapsed version — turns 0/1 knapsack into unbounded knapsack, allowing the same item to be picked multiple times.
  • Treating W as a free variable when it’s actually constrained by problem size — at W = 10^9 the table doesn’t fit; switch to meet-in-the-middle or branch-and-bound (out of scope here).

Classic Problems

  • LC 416 — Partition Equal Subset Sum (0/1 knapsack reformulation: target = total / 2). See Lab 03.
  • LC 494 — Target Sum.
  • LC 474 — Ones and Zeroes (2D knapsack).

10. Unbounded Knapsack

When To Use

Same as 0/1 knapsack but each item can be used any number of times. Recognized by “unlimited supply” / “any number of coins” / “items can be reused”.

State Design

dp[w] = best value with capacity w, considering all items as candidates at every step.

Transition

dp[w] = max(dp[w], dp[w - w_i] + v_i) for every item i such that w >= w_i.

Complexity

Time O(N · W). Space O(W). Iterate w left-to-right.

Common Pitfalls

  • Iterating wrong direction — same as 0/1 knapsack but inverted. Left-to-right makes items reusable; right-to-left makes them one-use.
  • Confusing “min number of items” with “max value” — in coin change (min coins), initialize to +∞, transition is dp[w] = min(dp[w], dp[w - c] + 1).
  • Counting orderings vs combinations: for “number of ways to make change as combinations”, the outer loop is over coins and inner over sums; for “number of ordered sequences”, swap them. The two produce different counts.

Classic Problems

  • LC 322 — Coin Change (min coins). See Lab 04.
  • LC 518 — Coin Change II (count combinations).
  • LC 279 — Perfect Squares.
  • LC 139 — Word Break (unbounded with “items” = dictionary words).

11. Subset Sum / Partition Equal Subset Sum

When To Use

“Can we pick a subset summing to T?” Recognized in: partition problems, target-sum problems, equal-sum-subsets.

Reformulation

Subset sum is 0/1 knapsack with v_i = w_i and target W = T. Use dp[w] = bool (reachable or not) instead of “max value”, and aggregate with OR instead of max.

Complexity

Time O(N · T). Space O(T) (bool array, can use a bitset for O(T / 64) space and time).

Common Pitfalls

  • Forgetting that target T may be huge — for “partition equal subset sum”, T = total / 2; if total is odd, return false immediately.
  • Using max instead of OR for boolean aggregation.

Classic Problems

  • LC 416 — Partition Equal Subset Sum. See Lab 03.
  • LC 698 — Partition to K Equal Sum Subsets (harder; bitmask DP).

12. LIS — Longest Increasing Subsequence

When To Use

“Longest subsequence with property P” where P is monotonic (increasing, non-decreasing, or some order relation).

State Design (O(N²) DP)

dp[i] = length of LIS ending at index i and using arr[i] as the last element.

Transition

dp[i] = 1 + max(dp[j] for j < i if arr[j] < arr[i]). Answer is max(dp[1..N]).

Complexity

O(N²) time, O(N) space.

Patience Sort / O(N log N) Variant

Maintain tails[k] = smallest possible tail of any increasing subsequence of length k+1. For each arr[i], find the leftmost tails[k] >= arr[i] via binary search and replace it with arr[i] (or append if arr[i] > all). The length of tails at the end is the LIS length.

This is patience sorting — laying cards onto piles where each pile is strictly decreasing top-to-bottom, and the number of piles is the LIS length.

Complexity

O(N log N) time, O(N) space.

Common Pitfalls

  • Confusing “LIS length” with “LIS itself”tails is not the LIS; reconstructing the actual sequence requires storing predecessors during scan.
  • Strict vs non-strict — for non-decreasing, use bisect_right instead of bisect_left.

Classic Problems

  • LC 300 — Longest Increasing Subsequence. See Lab 05.
  • LC 354 — Russian Doll Envelopes (sort + LIS).
  • LC 673 — Number of Longest Increasing Subsequences.

13. LCS / Edit Distance Family

When To Use

Two strings, asking for similarity, alignment, or transformation cost. Includes longest common subsequence, edit distance (Levenshtein), longest common substring (different state!), and shortest common supersequence.

State Design

dp[i][j] = answer for prefix-i of A and prefix-j of B.

Transitions

  • LCS: dp[i][j] = dp[i-1][j-1] + 1 if A[i-1] == B[j-1], else max(dp[i-1][j], dp[i][j-1]).
  • Edit distance (Levenshtein): if match, dp[i][j] = dp[i-1][j-1]; else 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) for replace / delete / insert.
  • Longest common substring (different!): if match, dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = 0. Answer is max(dp[i][j]) over all (i, j). The “else = 0” is what makes it substring vs subsequence.

Complexity

Time O(N · M). Space O(N · M) tabulated, O(M) with two rolling rows, O(M) with one row + a single saved diagonal value.

Common Pitfalls

  • Confusing subsequence and substring — they have different recurrences. Subsequence allows skipping; substring requires contiguity.
  • Edit distance with non-unit costs (insert/delete/replace each have a custom cost) — works the same with custom weights instead of +1.
  • Reconstructing the alignment requires backtracking through dp choices; store back-pointers or reconstruct from values.

Classic Problems

  • LC 1143 — Longest Common Subsequence.
  • LC 72 — Edit Distance. See Lab 06.
  • LC 583 — Delete Operation for Two Strings.
  • LC 712 — Minimum ASCII Delete Sum.
  • LC 718 — Maximum Length of Repeated Subarray (LCS variant; substring).

14. Palindrome DP

When To Use

Anything about palindromic substrings or subsequences: count, longest, partition into palindromes, minimum cuts.

Variant 1: Longest Palindromic Subsequence

dp[i][j] = length of longest palindromic subsequence in s[i..j].

dp[i][j] = dp[i+1][j-1] + 2          if s[i] == s[j]
         = max(dp[i+1][j], dp[i][j-1])  otherwise

Answer: dp[0][N-1]. Evaluation order: by interval length ascending.

Variant 2: Longest Palindromic Substring

is_pal[i][j] = boolean. is_pal[i][j] = (s[i] == s[j]) and (j - i < 2 or is_pal[i+1][j-1]). Track max length and start during fill.

(Manacher’s algorithm gives O(N) for this; see Phase 3.)

Variant 3: Palindrome Partitioning Min Cuts

cuts[i] = min cuts to partition s[0..i] into palindromes.

cuts[i] = 0                              if s[0..i] is itself a palindrome
        = min(cuts[j-1] + 1) for all j ≤ i with s[j..i] palindrome

Precompute is_pal[i][j] first (O(N²)), then run the cut DP (O(N²)). Total O(N²).

Common Pitfalls

  • Computing is_pal after the cut DP — circular dependency.
  • Wrong evaluation order in dp[i][j] — must fill smaller intervals first; iterate by length ascending.

Classic Problems

  • LC 516 — Longest Palindromic Subsequence. See Lab 07.
  • LC 5 — Longest Palindromic Substring.
  • LC 132 — Palindrome Partitioning II. See Lab 07.
  • LC 647 — Palindromic Substrings.

15. String DP

When To Use

Pattern matching with wildcards or operators: regex, glob/wildcard, interleaving, distinct subsequences. The state is two indices (one per string).

Variant: Regex Matching (LC 10)

dp[i][j] = does p[0..j] match s[0..i]?

if p[j-1] == '*':
    dp[i][j] = dp[i][j-2]                             # match zero of preceding
              or (matches(s[i-1], p[j-2]) and dp[i-1][j])  # match one more
elif matches(s[i-1], p[j-1]):
    dp[i][j] = dp[i-1][j-1]
else:
    dp[i][j] = False

Variant: Wildcard Matching (LC 44)

Similar but * matches any sequence: dp[i][j] = dp[i-1][j] or dp[i][j-1] when p[j-1] == '*'.

Variant: Interleaving Strings (LC 97)

dp[i][j] = can s3[0..i+j] be formed by interleaving s1[0..i] and s2[0..j]? Transition: take from s1 if s1[i-1] == s3[i+j-1]; take from s2 symmetrically; OR them.

Common Pitfalls

  • Off-by-one between pattern index and dp index — almost universal source of regex DP bugs.
  • * semantics differ between regex and glob; read the problem carefully.

Classic Problems

  • LC 10 — Regular Expression Matching.
  • LC 44 — Wildcard Matching.
  • LC 97 — Interleaving String.
  • LC 115 — Distinct Subsequences.

16. Tree DP

When To Use

The structure is a tree (rooted or rootable); the answer at a node depends on its subtree. Examples: house robber III, max path sum, longest path / diameter.

State Design

dp[v] = answer for the subtree rooted at v. Often a tuple: (best_with_v_chosen, best_without_v_chosen). Tuples are essential when the parent’s decision depends on whether the child was used.

Evaluation Order

Post-order DFS — fill children before the parent.

Transition

Aggregate over children. For house robber III: rob[v] = val[v] + sum(skip[c] for c in children); skip[v] = sum(max(rob[c], skip[c]) for c in children).

Complexity

Time O(V). Space O(V) for the recursion stack.

Common Pitfalls

  • Stack overflow at deep trees in Python (default limit 1000) — sys.setrecursionlimit(2 * 10**5) or convert to iterative post-order.
  • Mishandling N-ary vs binary children — N-ary requires summing over a dynamic list; binary is hard-coded (left, right).
  • Forgetting to handle null children — return identity values (0 or -∞).

Classic Problems

  • LC 337 — House Robber III. See Lab 08.
  • LC 124 — Binary Tree Maximum Path Sum.
  • LC 543 — Diameter of Binary Tree (variant).
  • LC 968 — Binary Tree Cameras (multi-state tree DP).

17. Interval DP

When To Use

The state is (left, right) — an interval — and the transition picks a “split point” k in [left, right]. Examples: matrix chain multiplication, burst balloons, palindrome partitioning, optimal BST, stone game.

State Design

dp[i][j] = answer for interval [i, j]. Often the meaningful question is “what is the last operation on this interval”, which forces a choice of split point k.

Transition

dp[i][j] = aggregate over k in [i..j]: dp[i][k-1] + dp[k+1][j] + cost(i, j, k).

The cost(i, j, k) typically depends on the boundaries of the interval — not just k — because the interval’s neighbors after the split are still i-1 and j+1.

Evaluation Order

By interval length ascending: for length in 1..N: for left in 0..N-length: right = left + length - 1.

Complexity

Time O(N³) in general (O(N²) intervals × O(N) split points). Space O(N²).

Common Pitfalls

  • Iterating (i, j) in the wrong order — must fill smaller intervals first. Length-ascending is the canonical order.
  • Choosing the wrong “thing” to split on — e.g., for burst balloons, the right state is “last balloon to burst in [i, j]” rather than “first balloon”.
  • Confusing the boundaries — the cost in burst balloons uses nums[i-1] and nums[j+1] as multipliers because those are the surviving neighbors at the moment the last balloon in [i, j] is burst.

Classic Problems

  • LC 312 — Burst Balloons. See Lab 09.
  • LC 1547 — Minimum Cost to Cut a Stick (matrix-chain-like).
  • LC 87 — Scramble String.
  • LC 132 — Palindrome Partitioning II. See Lab 07.

18. Bitmask DP

When To Use

Small-N (typically N ≤ 20) problems where the state must remember which subset of items has been used. Examples: TSP, assignment problem, set cover, “shortest path visiting all nodes”.

State Design

dp[mask] = best value over subsets specified by mask. Or dp[mask][last] = best path ending at node last and visiting exactly the nodes in mask (TSP-style).

Transition

For TSP: dp[mask | (1 << v)][v] = min(dp[mask | (1 << v)][v], dp[mask][u] + dist(u, v)) for all u in mask and v not in mask.

Evaluation Order

By mask value ascending — guarantees dp[submask] is filled before dp[mask] whenever submask ⊂ mask. Equivalently, by popcount(mask) ascending.

Complexity

Time O(2^N · N²) for TSP-style. Space O(2^N · N) — at N=20 this is 20 × 10^6 = 20M cells, fits in memory.

Common Pitfalls

  • Iterating bitmasks in the wrong order — by-mask-value ascending is the safe default.
  • Off-by-one on 1 << v vs 1 << (v-1) depending on 0- or 1-indexed nodes.
  • Forgetting mask includes the source when initializing.
  • Underestimating memory — at N=22, 2^N × N = 92M cells; at N=24, 400M+. Bitmask DP is strictly small-N.

Classic Problems

  • LC 847 — Shortest Path Visiting All Nodes. See Lab 10.
  • LC 943 — Find the Shortest Superstring.
  • LC 1349 — Maximum Students Taking Exam (bitmask over rows).
  • LC 1125 — Smallest Sufficient Team.

19. Digit DP (Overview)

When To Use

“Count numbers in [L, R] with property P” where P is digit-defined (sum of digits, no consecutive equal, contains a digit, etc.). The state is (position, tight, accumulator…).

State Design

dp[pos][tight][...accumulated state] where tight is a flag indicating whether the prefix so far equals the upper bound’s prefix (so the next digit is bounded).

Transition

For each digit d in 0..(9 if not tight else upper_bound[pos]), recurse to pos + 1 with tight' = tight and d == upper_bound[pos], updating the accumulator.

Complexity

Time O(D × 2 × digit_range × accumulator_size), typically tractable for D = 18 (decimal) and small accumulator.

Common Pitfalls

  • Off-by-one between [L, R] and [0, R] — answer is count(R) - count(L-1).
  • Leading zeros — track a “started” flag; otherwise “001” and “1” are conflated.
  • Memoizing on tight=True paths — they’re path-specific and shouldn’t be memoized; only memoize the tight=False branch.

Classic Problems

  • LC 233 — Number of Digit One.
  • LC 902 — Numbers At Most N Given Digit Set.
  • LC 1012 — Numbers With Repeated Digits.

Overview-only in this phase; depth in Phase 7 (Competitive Programming).


20. DP On DAG

When To Use

The graph is acyclic; you want longest / shortest / count of paths. The DAG itself defines the topological order; the DP runs along it.

State Design

dp[v] = answer for paths ending at v (or starting from v).

Transition

For longest path: dp[v] = max(dp[u] + w(u, v) for u in predecessors(v)). Run in topological order on the DAG.

Complexity

Time O(V + E). Space O(V).

Common Pitfalls

  • Running on a graph that has cycles — the recurrence diverges or memoization loops. Confirm DAG-ness with topological sort first.
  • Confusing “longest path” (NP-hard in general graphs) with “longest path in a DAG” (polynomial) — always say “in a DAG” out loud.

Classic Problems

  • LC 329 — Longest Increasing Path in a Matrix (implicit DAG).
  • LC 1857 — Largest Color Value in a Directed Graph.
  • “Longest path in a DAG” — folklore.

21. Game DP (Minimax / Nim / Stone Game)

When To Use

Two-player zero-sum perfect-information game; ask whether the first player wins, or by what margin. Examples: stone game, Nim, predict-the-winner.

State Design

dp[state] = the optimal score the current player can guarantee, assuming both play optimally. Often dp[i][j] with i, j being the two ends of a contested range.

Transition

The current player picks the choice that maximizes their own score. The opponent then plays from the resulting state, also optimally — so the value at the resulting state is what the opponent nets, not the current player. Hence:

dp[i][j] = max(stones[i] - dp[i+1][j], stones[j] - dp[i][j-1])

The -dp[...] flips perspective — the opponent’s optimal score becomes a deduction from the current player’s view.

Common Pitfalls

  • Forgetting the perspective flip+dp[...] instead of -dp[...]. Produces nonsensical “both players cooperate” answers.
  • Confusing “current player wins” with “first player wins”dp[state] is from the perspective of whoever moves at this state, which may not be the original first player after several moves.

Classic Problems

  • LC 486 — Predict the Winner.
  • LC 877 — Stone Game.
  • LC 1140 — Stone Game II.
  • LC 464 — Can I Win (game DP + bitmask).

22. Probability And Expected Value DP

When To Use

Random walks, expected number of steps, probability of reaching a state. Examples: knight probability, dice problems, Markov chains in disguise.

State Design

dp[state] = probability of being in state after the random process, OR expected value of some random variable from state.

Transition

For probability: dp[next] = sum(P(s -> next) × dp[s]) over all predecessors. For expected value (with stopping): E[state] = expected_immediate + sum(P(s -> next) × E[next]) for non-terminal states; E[terminal] = 0.

Complexity

Same as the underlying state-space DP.

Common Pitfalls

  • Conflating probability DP and expected-value DP — they have different recurrences; pick the right one for the question.
  • Numerical stability — many small probabilities multiplied; use log or rational arithmetic when extreme.
  • Infinite expected steps — if there’s a non-zero probability of never reaching the terminal, the expected value is infinite; check reachability first.

Classic Problems

  • LC 688 — Knight Probability in Chessboard.
  • LC 837 — New 21 Game.
  • “Expected number of dice rolls to reach sum N” — folklore.

DP-Recognition Cheat Sheet

The hardest skill in this phase is recognizing that a problem is DP. Here is a battery of signals.

Signal in problem statementLikely DP variant
“Count number of ways”Counting DP — sum over choices
“Maximum / minimum cost” with sequential choicesOptimization DP
“Pick subset with property P” / “partition”Subset / knapsack
“Longest / shortest subsequence”LIS / LCS family
“Edit / transform A into B”Edit distance family
“Each item used at most once”0/1 knapsack
“Each item can be reused”Unbounded knapsack
“Substring / subarray / contiguous”1D DP (often Kadane-like)
“Subsequence (non-contiguous)”LCS / LIS family
“Palindromic”Interval DP, expand-around-center, or LCS(s, rev(s))
“Match a pattern with * / .Regex / wildcard DP
“Tree” + “subtree answer aggregates”Tree DP, post-order
“N ≤ 20” + “visit all” / “subset”Bitmask DP
“N ≤ 100” + “split into intervals” / “merge intervals”Interval DP, length-ascending
“Two-player game, both optimal”Game DP, perspective-flip
“Probability” / “expected” + “random walk / dice”Probability/EV DP
“Number of digits ≤ 18, range [L, R]”Digit DP
“Acyclic graph + longest/count paths”DP on DAG
“Climbing / hopping with steps {a, b, c}”1D DP, Fibonacci-like
“Decide YES/NO with budget K”Reachability DP, often boolean knapsack

Common DP Bugs

A taxonomy. Each one shows up in at least 30% of submitted DP solutions.

  1. Wrong base case. dp[0] initialized to 0 when it should be 1 for counting, or 0 when it should be +∞ for min. Check by running tabulated against memoized on N=0, 1, 2.
  2. Wrong evaluation order. 2D DP iterated in (j, i) order when the recurrence reads dp[i-1][j]. Interval DP iterated in (left, right) instead of (length, left). Bitmask DP iterated in arbitrary mask order.
  3. Off-by-one between value-array index and DP index. If dp[i] is “first i elements”, the latest element is arr[i-1], not arr[i]. If dp[i] is “ending at index i”, the latest element is arr[i]. Pick one and never mix.
  4. Missing a choice in the transition. The “skip” / “do nothing” choice is the most-often-forgotten. Without it, you over-constrain the answer.
  5. Wrong direction in 1D-collapsed knapsack. Left-to-right (unbounded) vs right-to-left (0/1). Silently flipping turns one problem into the other.
  6. Counting orderings instead of combinations. In coin change variants, the loop nesting (coins outer vs sums outer) determines combinations vs permutations.
  7. Not handling unreachable states. +∞ propagation: if you compute dp[w] = dp[w - c] + 1 and dp[w-c] = +∞, your dp[w] becomes a large finite number (in fixed-width integer types) — overflow. Use INF = 10^9 + 7 and guard with explicit if dp[w-c] == INF: continue.
  8. Recursion stack overflow in Python at N > 1000 — convert to iterative, or sys.setrecursionlimit(10**6) and accept memory cost.
  9. Memoizing on mutable arguments. lru_cache requires hashable args; lists / dicts must be tuples / frozensets.
  10. Wrong perspective flip in game DP. +dp[...] instead of -dp[...]. Both players appear to cooperate in your model.
  11. Including or excluding the boundary of the table inconsistently. Off-by-one in iterators, inclusive/exclusive bounds.
  12. Time / space estimate ignoring constants. “O(N · M) at N = M = 10^4” is 10^8 — TLE in Python, fine in C++. State the constant honestly.

Mastery Checklist

Before exiting this phase, verify all of these:

  • You can derive a state from a recursive brute force in <3 minutes for any DP problem.
  • You can write the recurrence (transition) in <2 minutes once the state is fixed.
  • You execute the brute → memo → tabulated → space-optimized progression on every DP problem in this phase, without skipping stages.
  • You can write tabulated 1D DP (house robber, climbing stairs) in <5 minutes from a blank screen.
  • You can write tabulated 2D DP (unique paths, edit distance) in <8 minutes from a blank screen.
  • You can space-optimize 2D DP to O(M) on demand, including the right-to-left collapse trick for 0/1 knapsack.
  • You can implement LIS at O(N²) and at O(N log N) in <15 minutes total.
  • You can implement edit distance with full progression in <25 minutes.
  • You can implement house robber III (tree DP) with the (rob, skip) tuple pattern in <15 minutes.
  • You can implement burst balloons (interval DP) with the length-ascending iteration in <25 minutes.
  • You can implement TSP-style bitmask DP (dp[mask][last]) in <30 minutes.
  • You can articulate why iterating for j: for i: in 2D DP can produce garbage — i.e., the topological-order argument — in <30 seconds.
  • You can articulate why 0/1 knapsack iterates w right-to-left and unbounded iterates left-to-right — in <30 seconds.
  • You can articulate the perspective-flip in game DP — in <30 seconds.

Exit Criteria

You may move to Phase 6 (Greedy and Mathematical Thinking) when all of the following are true:

  1. You have completed all ten labs in this phase, with each lab’s mastery criteria checked off.
  2. You have solved at least 50 unaided DP problems from LeetCode (mix of Medium, Medium-Hard, Hard) and reviewed each via REVIEW_TEMPLATE.md.
  3. Your unaided success rate on Medium-Hard DP problems is ≥ 65%.
  4. In a mock interview (phase-11-mock-interviews/), you correctly identify the DP variant within 2 minutes for at least 7 of 10 DP problems and produce the recurrence within 4 minutes for at least 6 of 10.
  5. You execute the brute → memo → tabulated → space-optimized progression on every DP problem in mocks, even when the interviewer doesn’t ask for all four stages — this is the single discipline of this phase, and skipping it is a phase-failure.

If any of these fails, do another 20–30 DP problems before moving on. Skipping this gate calcifies bad habits that destroy you in Phase 7 (competitive programming) where DP shows up at every turn.


Labs

Hands-on practice. Each lab follows the strict 22-section format and demonstrates the four-stage progression in detail.

  1. Lab 01 — 1D DP Foundations (House Robber)
  2. Lab 02 — 2D DP (Unique Paths with Obstacles)
  3. Lab 03 — 0/1 Knapsack (Partition Equal Subset Sum)
  4. Lab 04 — Unbounded Knapsack (Coin Change)
  5. Lab 05 — LIS (Longest Increasing Subsequence)
  6. Lab 06 — LCS / Edit Distance
  7. Lab 07 — Palindrome DP (LPS + Min Cuts)
  8. Lab 08 — Tree DP (House Robber III)
  9. Lab 09 — Interval DP (Burst Balloons)
  10. Lab 10 — Bitmask DP (Shortest Path Visiting All Nodes)

← Phase 4: Graph Mastery · Phase 6: Greedy → · Back to Top