LC Patterns — Advanced DP (2D and Beyond)

Companion to 1d dp (the 5-question framework there applies verbatim here) · Code: C++
Per-pattern template: What it is → Intuition → Approach → Challenges & pitfalls → LC problems (max 3) → Worked examples.

The Master Insight (read before the patterns)

Higher-dimensional DP is not a new technique — it's the same 5-question framework where the state needs more than one number to be self-contained. The art is choosing dimensions: ask "what must I remember for the subproblem to be answerable without looking outside it?" Each pattern below is one canonical answer:

The state must remember…DimensionsPattern
position in a grid(row, col)1, 2
positions in two sequences(i, j)3, 4
a range of one sequence(l, r)5, 6, 9
position + remaining budget(s)(i, b₁[, b₂])7, 8
which subset has been used(mask)10
a split point inside a count(n) via splits11
digit position + tightness(pos, tight, …)12

Two skills join the framework at ≥ 2D:

  1. Traversal order comes from the dependency arrows, not habit. Grids go top-left→bottom-right unless the base is at the goal (Dungeon Game: backwards). Interval DP must iterate by increasing range length. When the order is unclear, write top-down memoization — recursion finds the order for you (and for sparse state spaces like regex, it's genuinely faster).
  2. Space compression is a dimension-collapse: if dp[i][*] reads only dp[i-1][*], keep two rows (or one row + careful direction — the 0/1-knapsack backwards trick from the 1D file, generalized).
Tree DP is covered in trees bsts Pattern 9 (return/record split, robber-on-tree) — the state there is a node plus a small enum, same framework.

§0Reference Skeletons & Universal Pitfalls

2D bottom-up with padded borders (the default shape)

vector<vector<long long>> dp(m + 1, vector<long long>(n + 1, 0));  // row/col 0 = empty prefix
for (int i = 1; i <= m; ++i)
    for (int j = 1; j <= n; ++j)
        dp[i][j] = combine(dp[i-1][j], dp[i][j-1], dp[i-1][j-1], cost(i, j));

Top-down memo (order-agnostic; best for sparse/complex transitions)

long long f(int i, int j, vector<vector<long long>>& memo) {
    if (base(i, j)) return baseVal;
    auto& res = memo[i][j];
    if (res != -1) return res;
    return res = /* transition over choices */;
}
  1. Padding vs raw indexing: size (m+1)×(n+1) with row/col 0 as "empty prefix" removes every boundary if from LCS/edit-distance transitions. Use it by default for sequence DP; state the convention aloud.
  2. Dependency-order bugs: interval DP iterated by l then r (instead of by length) reads uncomputed cells and fails quietly on some inputs. Derive the order from arrows: dp[l][r] needs shorter intervals ⇒ loop len = 2..n outermost.
  3. Memo sentinel collisions: -1 as "uncomputed" breaks when −1 is a legal answer (game DP with negative scores) — use a separate seen array or LLONG_MIN.
  4. O(n²) states × O(n) transition = O(n³): interval DP is usually cubic — say the complexity before coding so nobody expects magic; n ≤ ~500 is the constraint hint that cubic is intended.
  5. Compressed-row aliasing: when collapsing to one row, dp[j-1] may be this row's value (new) while dp[j] is the last row's (old) — edit distance needs a saved prevDiag temp. If the juggling gets confusing in an interview, keep two rows: clarity > 2× memory.
  6. Counting DPs overflow: paths/ways in a 2D grid explode combinatorially — long long or the problem's modulus from the first line, not after the WA.

1Grid Path DP (unique paths, min path sum)

What it is: Walk a grid moving only right/down: count the paths, or minimize the sum of visited cells. dp[i][j] = answer for reaching cell (i,j); the transition pulls from top and left.

Intuition: Movement restrictions (right/down only) make the grid a DAG — every cell's answer depends only on two already-computed neighbors, so one row-major sweep settles everything. Split-by-last-step again: any path into (i,j) arrived from above or from the left; counts add, costs min. Obstacles (63) just zero out cells. This is the 2D "hello world" — drill until the compressed one-row version is automatic.

Approach:

int minPathSum(vector<vector<int>>& g) {
    int m = g.size(), n = g[0].size();
    vector<int> dp(n, INT_MAX); dp[0] = 0;
    for (int i = 0; i < m; ++i)
        for (int j = 0; j < n; ++j) {
            if (j > 0) dp[j] = min(dp[j], dp[j-1]);   // dp[j] holds top (old), dp[j-1] left (new)
            dp[j] = (i || j) ? dp[j] + g[i][j] : g[0][0];
        }
    return dp[n-1];
}

Challenges & pitfalls

first row/column initialization (only one way in — the classic slip is an obstacle in row 0 blocking everything after it); the one-row compression's old/new semantics (§0.5); counting variants overflow (§0.6); 62 also has the closed-form C(m+n−2, m−1) — name it as the math alternative.

LC 62 Unique PathsLC 64 Minimum Path SumLC 63 Unique Paths II
Example 1: Minimum Path Sum (LC 64) — grid
1 3 1
1 5 1
4 2 1

moving only right/down, minimize the total.

  • dp[i][j] = cheapest cost to reach (i,j) = its own value + the cheaper of (from above, from left):
    • Row 0 (only from left): 1, 4, 5. Column 0 (only from above): 1, 2, 6.
    • (1,1): 5 + min(4, 2) = 7. (1,2): 1 + min(5, 7) = 6. (2,1): 2 + min(7, 6) = 8. (2,2): 1 + min(6, 8) = 7.
  • Answer 7 (path 1 → 3 → 1 → 1 → 1). ✓ Each cell only needed its two settled neighbors.

2Grid DP Twists (backwards DP, shape DP)

What it is: Grid DP where the direction or the meaning twists: Dungeon Game (174) computes the minimum health needed — solvable only backwards from the princess; Maximal Square (221) redefines the state as a shape anchored at the cell — "the side of the largest square whose bottom-right corner is here."

Intuition: 174 teaches that the base case's location dictates the direction: "health needed at (i,j)" depends on where you're going, not where you've been — forward DP fails because future rooms can refund health (clamping breaks optimal substructure); backwards, need[i][j] = max(1, min(need[right], need[down]) − dungeon[i][j]) is clean. 221 teaches anchored-shape states: a square of side s at corner (i,j) requires squares of side s−1 at the three neighboring corners ⇒ dp[i][j] = 1 + min(top, left, topleft) — the min of three smaller shapes is the geometry.

Approach (221 core):

// dp[i][j] = side of largest all-1s square with bottom-right corner at (i,j)
if (g[i][j] == '1')
    dp[i][j] = 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
best = max(best, dp[i][j]);            // answer: best², recorded globally

Challenges & pitfalls

174's two clamps (never below 1 — both at the goal and per step; the answer is need[0][0], computed right-to-left, bottom-to-top); trying 174 forward and patching (it can't be patched — be ready to explain why forward loses information); 221 returns the side, the answer is the area (read the ask); "largest rectangle of 1s" (85) is not this pattern — it's histogram/monotonic-stack; route correctly.

LC 174 Dungeon GameLC 221 Maximal SquareLC 931 Minimum Falling Path Sum
Example 1: Maximal Square (LC 221) — matrix
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
  • dp[i][j] = side of the largest all-1s square whose bottom-right corner is (i,j) = 1 + min(top, left, top-left) if the cell is 1.
  • Working through the grid, the cell at row 2, col 3 becomes 1 + min(1, 1, 1) = 2 — a 2×2 square of 1s sits with its corner there (rows 1–2, cols 3–4).
  • Largest side = 2 → area = 2² = 4. ✓ The "min of the three neighbors + 1" is exactly the geometry of a square needing all three smaller squares present.

3Two-Sequence Alignment (LCS / Edit Distance)

What it is: Compare two sequences position by position: longest common subsequence, minimum edits to transform one into the other, counting distinct subsequence matches. dp[i][j] = answer for prefixes a[0..i) and b[0..j); the transition branches on whether a[i-1] == b[j-1].

Intuition: Split by what happens to the last characters: they match (consume both — a diagonal move) or one is skipped/edited (drop from a, drop from b, or substitute — the three edit-distance ops map exactly to the three neighbor cells: dp[i-1][j] = delete, dp[i][j-1] = insert, dp[i-1][j-1] = replace/match). Nearly every two-string problem is LCS or edit distance in costume — "min deletions to equalize" (583) = lengths − 2·LCS.

Approach:

int minDistance(string a, string b) {
    int m = a.size(), n = b.size();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1));
    for (int i = 0; i <= m; ++i) dp[i][0] = i;      // delete all of a's prefix
    for (int j = 0; j <= n; ++j) dp[0][j] = j;      // insert all of b's prefix
    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= n; ++j)
            dp[i][j] = (a[i-1] == b[j-1])
                ? dp[i-1][j-1]
                : 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
    return dp[m][n];
}

Challenges & pitfalls

the i-1 string-index vs i dp-index offset (padding convention, §0.1 — mixing them is the bug); base row/column meaning (empty-vs-prefix — derive, don't memorize); 115 (distinct subsequences) changes the combine to addition with an asymmetric skip + overflow; subsequence (gaps ok, this pattern) vs substring (contiguous — different recurrence) — mislabeling wastes ten minutes.

LC 1143 Longest Common SubsequenceLC 72 Edit DistanceLC 115 Distinct Subsequences
Example 1: Longest Common Subsequence (LC 1143)a = "abcde", b = "ace".
  • dp[i][j] = LCS length of the first i of a and first j of b. When the last chars match, take the diagonal + 1; else the better of dropping one char from either side.
    • a and ac share a (1); abc and ace: c matches → 1 + (LCS of ab,a) = 2; abcde and ace: e matches → 1 + (LCS of abcd,ac = 2) = 3.
  • LCS = 3 (the subsequence "ace"). ✓ Matching characters move diagonally; mismatches drop a character from one prefix.

4Wildcard / Regex Matching

What it is: Two-sequence DP where one side is a pattern with metacharacters: ?/. (any single char), * (wildcard: any sequence; regex: zero-or-more of the preceding element). dp[i][j] = "does s[0..i) match p[0..j)".

Intuition: Still last-decision partitioning, but * branches specially. Wildcard * (44): matches empty (dp[i][j-1]) or eats one more char and stays (dp[i-1][j]). Regex x* (10) is a unit: use zero copies (dp[i][j-2]) or, if x matches s[i-1], one more copy (dp[i-1][j]). The empty-pattern-prefix base row (dp[0][j]: only satisfied by chains of x* units) is half the marks.

Approach (10's transition, the part to memorize):

if (p[j-1] == '*')
    dp[i][j] = dp[i][j-2]                                        // zero copies of the unit
        || (i > 0 && matches(s[i-1], p[j-2]) && dp[i-1][j]);     // one more copy
else
    dp[i][j] = i > 0 && matches(s[i-1], p[j-1]) && dp[i-1][j-1];

Challenges & pitfalls

regex * binds to the previous char — treating it like wildcard * is the wrong answer; the dp[0][j] base loop (empty string vs "a*b*c*" = true); j-2 accesses need the pattern-validity assumption (* never first); top-down memo is honestly easier to get right here (§0's advice); if the interviewer says "implement regex," clarify the supported subset first.

LC 10 Regular Expression MatchingLC 44 Wildcard Matching
Example 1: Regular Expression Matching (LC 10)s = "aa", p = "a*".
  • a* is one unit meaning "zero or more as." Two options at the *:
    • use zero copies → pattern would be empty, but s = "aa" isn't → no.
    • use one more copy since a matches s's next char → reduces to matching "a" against "a*", then "" against "a*" (zero copies) → true.
  • "aa" matches "a*". ✓ Contrast s = "aa", p = "a": no *, lengths force a char-by-char match and the second a has nothing to match → false. The key is that * attaches to the char before it.

5Palindrome DP on One String (the (l, r) state)

What it is: Problems about palindromic subsequences (deletions allowed) inside one string: longest palindromic subsequence, min insertions to make the whole string a palindrome. State = a substring range (l, r); the transition compares the endpoints.

Intuition: Endpoints equal → they wrap a smaller problem: dp[l][r] = dp[l+1][r-1] + 2. Endpoints differ → one of them isn't in the optimal palindrome: max(dp[l+1][r], dp[l][r-1]). That two-case endpoint recursion is the whole family; 1312's cute reduction: min insertions = n − LPS(s), and LPS(s) itself = LCS(s, reverse(s)), tying Patterns 3 and 5 together. Subsequence here vs substring (5/647, expand-from-center) — deletions allowed vs contiguity, always route first.

Approach:

int longestPalindromeSubseq(string s) {
    int n = s.size();
    vector<vector<int>> dp(n, vector<int>(n, 0));
    for (int l = n - 1; l >= 0; --l) {          // l descending, r ascending ⇒ deps ready
        dp[l][l] = 1;
        for (int r = l + 1; r < n; ++r)
            dp[l][r] = (s[l] == s[r]) ? dp[l+1][r-1] + 2
                                      : max(dp[l+1][r], dp[l][r-1]);
    }
    return dp[0][n-1];
}

Challenges & pitfalls

iteration order (l descending / r ascending, or by length — both respect the arrows; picking randomly doesn't — §0.2); the l+1 > r-1 empty-interval read (the zero-initialized table absorbs it — but know you're relying on that); the adjacent-equal case ("aa": dp[l+1][r-1] is the empty interval = 0, +2 = 2 ✓).

LC 516 Longest Palindromic SubsequenceLC 1312 Min Insertions to Make PalindromeLC 647 Palindromic Substrings — DP-table view
Example 1: Longest Palindromic Subsequence (LC 516)s = "bbbab".
  • dp[l][r] = longest palindromic subsequence in s[l..r]. When the two ends match, add 2 to the inside; else take the best of dropping one end.
    • Every single char is a palindrome of length 1. The ends of "bbbab" are b and b → match → 2 + LPS("bba").
    • "bba": ends b/a differ → max(LPS("ba"), LPS("bb")) = 2 (the "bb").
    • So LPS("bbbab") = 2 + 2 = 4.
  • Answer 4 (the subsequence "bbbb"). ✓ Matching ends wrap a smaller palindrome; mismatched ends drop whichever end helps least.

6Interval DP (partition by the LAST event)

What it is: An operation consumes elements of a range and its cost depends on what's around — burst balloons, cut a stick, triangulate a polygon. dp[l][r] = best for the open range (l, r); the transition picks which element acts last.

Intuition: The famous inversion: choosing the first balloon to burst entangles subproblems (its neighbors change for everything after). Choosing the last balloon k decouples them: when k finally bursts, its neighbors are exactly the range borders l and r — known! — and (l,k), (k,r) were solved independently before that. dp[l][r] = max over k of dp[l][k] + dp[k][r] + a[l]·a[k]·a[r]. "Think about the last decision, not the first" is the single transferable insight of interval DP.

Approach:

int maxCoins(vector<int>& nums) {
    vector<int> a = {1};                          // pad virtual 1s at both borders
    for (int x : nums) a.push_back(x);
    a.push_back(1);
    int n = a.size();
    vector<vector<int>> dp(n, vector<int>(n, 0)); // dp[l][r]: open interval (l, r)
    for (int len = 2; len < n; ++len)             // by increasing length — mandatory
        for (int l = 0; l + len < n; ++l) {
            int r = l + len;
            for (int k = l + 1; k < r; ++k)
                dp[l][r] = max(dp[l][r], dp[l][k] + dp[k][r] + a[l]*a[k]*a[r]);
        }
    return dp[0][n-1];
}

Challenges & pitfalls

open-vs-closed interval convention (open (l,r) with padded borders is cleanest — declare it and never waver); length-outermost iteration (§0.2 — the silent-failure bug); O(n³) is expected (§0.4); recognizing the family from the coupling ("each operation's cost depends on current neighbors" ⇒ think last-event); matrix-chain multiplication is the textbook ancestor.

LC 312 Burst BalloonsLC 1547 Minimum Cost to Cut a StickLC 1039 Minimum Score Triangulation
Example 1: Burst Balloons (LC 312)nums = [3, 1, 5, 8]; bursting balloon i earns left · i · right of its current neighbors.
  • The trick: decide which balloon bursts last in a range. When it bursts last, its neighbors are exactly the (virtual 1) borders, so the two sides are independent. Pad to [1, 3, 1, 5, 8, 1].
  • The optimal order works out to: burst 1 (earn 3·1·5=15), then 5 (earn 3·5·8=120), then 3 (earn 1·3·8=24), then 8 (earn 1·8·1=8), total 15 + 120 + 24 + 8 = 167.
  • Answer 167. ✓ Conditioning on the last balloon in each range is what keeps the neighbors well-defined; conditioning on the first would entangle everything.

7Multi-Constraint Knapsack & Target Counting

What it is: Knapsack with two budgets (474: strings cost zeros AND ones; capacity is m zeros, n ones) or counting ways to hit a target with grouped choices (1155: k dice, f faces, sum target). State = position + all remaining budgets.

Intuition: Each independent constraint is one dimension — 474 is literally 0/1 knapsack with a 2D capacity, compressed in place by iterating both budgets backwards. Dice counting (1155) shows the "grouped choices" shape: each die must contribute exactly one face — dp[d][t] = Σ_f dp[d-1][t-f] — a bounded, mandatory pick per stage (contrast: knapsack items are optional). Identifying optional-vs-mandatory and bounded-vs-unbounded per stage tells you which knapsack you're in.

Approach (474 core):

// dp[z][o] = max strings kept with z zeros and o ones of budget
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (auto& s : strs) {
    int z = count(s.begin(), s.end(), '0'), o = s.size() - z;
    for (int i = m; i >= z; --i)                  // both dims backwards: 0/1 semantics
        for (int j = n; j >= o; --j)
            dp[i][j] = max(dp[i][j], dp[i - z][j - o] + 1);
}
return dp[m][n];

Challenges & pitfalls

backwards iteration on every compressed dimension (forwards on either one silently allows reuse); modulus discipline in counting variants (% 1'000'000'007 on every addition); dimensional cost math before coding; profit-threshold variants (879) clamp the profit dimension at the threshold to keep it bounded.

LC 474 Ones and ZeroesLC 1155 Number of Dice Rolls With Target SumLC 879 Profitable Schemes
Example 1: Ones and Zeroes (LC 474)strs = ["10", "0001", "111001", "1", "0"], budget m = 5 zeros and n = 3 ones. Largest subset you can afford?
  • Each string costs (its zeros, its ones); this is 0/1 knapsack with two capacities. dp[z][o] = max strings kept spending ≤ z zeros and ≤ o ones.
  • Apply each string, updating both budget dimensions backwards (so each string is used once). The best affordable subset here is ["10", "0001", "1", "0"] — costs 4 zeros and 3 ones, all within budget → 4 strings.
  • Answer 4. ✓ Two independent constraints = two dimensions; iterating both backwards keeps each string single-use.

8Dimension-Augmented State Machines (stock with k transactions)

What it is: The 1D file's state-machine pattern (hold/sold/rest) gains a counted resource: at most k buy-sell transactions. State = (day, transactions used, holding?) — the day axis rolls away, leaving a small 2D table updated per price.

Intuition: When a constraint is "at most k times," k becomes a dimension — the general rule for upgrading any state machine. The recurrences are buy[j] = max(buy[j], sell[j-1] − price) and sell[j] = max(sell[j], buy[j] + price). Special cases collapse: k=1 is 121, k=∞ is 122 (greedy), k=2 is 123 — and k ≥ n/2 is k=∞ (a transaction needs 2 days), the pruning that saves 188 from a huge k.

Approach (188, compact):

int maxProfit(int k, vector<int>& prices) {
    int n = prices.size();
    if (k >= n / 2) { /* unlimited: sum all positive deltas */ }
    vector<int> buy(k + 1, INT_MIN), sell(k + 1, 0);
    for (int p : prices)
        for (int j = 1; j <= k; ++j) {
            buy[j]  = max(buy[j],  sell[j-1] - p);
            sell[j] = max(sell[j], buy[j] + p);
        }
    return sell[k];
}

Challenges & pitfalls

INT_MIN + arithmetic (trace the first iteration; the paranoid fix is long long); when a transaction "counts" (on buy or on sell — pick one, keep the recurrences consistent); the k ≥ n/2 collapse (without it, k = 10⁹ TLEs — the intended trap of 188); same-day buy-sell semantics (the in-day j loop ordering permits it).

LC 123 Best Time to Buy and Sell Stock IIILC 188 …IV
Example 1: Best Time to Buy and Sell Stock III (LC 123)prices = [3, 3, 5, 0, 0, 3, 1, 4], at most k = 2 transactions.
  • The extra dimension is "transactions used so far." Track, for each transaction slot, the best profit after buying and after selling.
  • The best two non-overlapping trades: buy at 0 (the price-0 day) and sell at 3, then buy at 1 and sell at 4 → (3 − 0) + (4 − 1) = 3 + 3 = 6.
  • Answer 6. ✓ Capping at k transactions is just one more index on the state machine; k ≥ n/2 would collapse to the unlimited (greedy) case.

9Game Theory DP (minimax on turns)

What it is: Two players alternate optimally — predict the winner or the final score difference. State = the remaining position (usually a range (l, r)); value = best score margin for whoever moves now.

Intuition: The "margin" formulation kills the two-player bookkeeping: dp[l][r] = max(a[l] − dp[l+1][r], a[r] − dp[l][r−1]) — take an end, minus the opponent's best margin on what remains (their gain is your loss; the negation is the turn switch). The first player wins iff dp[0][n-1] ≥ 0. For cost-guarantee games (375), the flip is max-over-adversary, min-over-you.

Approach:

// 486: dp[l][r] = current mover's best margin on a[l..r]
for (int l = n - 1; l >= 0; --l) {
    dp[l][l] = a[l];
    for (int r = l + 1; r < n; ++r)
        dp[l][r] = max(a[l] - dp[l+1][r], a[r] - dp[l][r-1]);
}
return dp[0][n-1] >= 0;

Challenges & pitfalls

tracking two scores instead of the margin (doubles the state for nothing — the margin trick is the pattern); sentinel −1 collides with legal negative margins (§0.3); 877's punchline: the DP works but parity/symmetry proves player one always wins — mention both; "optimal play" must be total (your max assumes their max).

LC 486 Predict the WinnerLC 877 Stone GameLC 375 Guess Number Higher or Lower II
Example 1: Predict the Winner (LC 486)nums = [1, 5, 2]; players alternately take an end; does player 1 win (ties count as a win)?
  • dp[l][r] = the current mover's best margin (own points minus opponent's) on nums[l..r]:
    • singles: dp[0][0]=1, dp[1][1]=5, dp[2][2]=2.
    • dp[0][1] ([1,5]): max(1 − 5, 5 − 1) = 4. dp[1][2] ([5,2]): max(5 − 2, 2 − 5) = 3.
    • dp[0][2]: max(1 − dp[1][2], 2 − dp[0][1]) = max(1 − 3, 2 − 4) = −2.
  • Margin −2 < 0 → player 1 losesfalse. ✓ Subtracting the opponent's best margin on the remainder is what encodes "it's their optimal turn next."

10Bitmask DP (the subset-as-state)

What it is: When "which items are already used" is unavoidable state and n ≤ ~20, encode the subset as an integer bitmask: dp[mask] (often dp[mask][last]). Assignment problems, minimum work sessions, visiting all nodes.

Intuition: 2ⁿ subsets fit in an int, and subset transitions are bit ops: add item i → mask | (1 << i); is i used → mask >> i & 1. The n ≤ 20 constraint in a problem statement is the pattern announcing itself. Two standard shapes: fill-by-subset (dp[mask] = best for exactly this set — iterate masks ascending) and BFS-over-masks when it's shortest-path-shaped (847: state = (node, visited-mask), plain BFS).

Approach (1986-style skeleton):

// dp[mask] = min sessions (and time used in the last session) covering tasks in mask
vector<pair<int,int>> dp(1 << n, {INT_MAX, INT_MAX});   // (sessions, lastSessionTime)
dp[0] = {1, 0};
for (int mask = 0; mask < (1 << n); ++mask)
    for (int i = 0; i < n; ++i) {
        if (mask >> i & 1) continue;
        auto [s, t] = dp[mask];
        int nm = mask | (1 << i);
        pair<int,int> cand = (t + task[i] <= T) ? make_pair(s, t + task[i])
                                                : make_pair(s + 1, task[i]);
        dp[nm] = min(dp[nm], cand);                     // lexicographic min works here
    }

Challenges & pitfalls

complexity honesty (O(2ⁿ·n) ≈ 20M for n=20: fine; O(3ⁿ) submask enumeration for n=20 ≈ 3.5B: not — know the for (sub = mask; sub; sub = (sub-1) & mask) idiom and its cost); mask iteration order (ascending guarantees predecessors done for add-one-bit transitions); pair-state design (justify why lexicographic min is safe); memory at n = 22+ (check before committing).

LC 1986 Minimum Number of Work SessionsLC 847 Shortest Path Visiting All NodesLC 698 Partition to K Equal Sum Subsets — bitmask alternative
Example 1: Shortest Path Visiting All Nodes (LC 847) — an undirected graph on n = 4 nodes; find the shortest walk visiting every node (revisits allowed), starting anywhere.
  • A plain "which node am I at" state isn't enough — you also need "which nodes have I visited." So the state is (node, visited-mask), and because every edge costs 1, this is unweighted shortest path → BFS over these 4 × 2⁴ = 64 states.
  • Seed the queue with (i, 1<<i) for every start node i. The first time any state reaches mask == 1111 (all four bits set), its BFS depth is the answer — for a path graph 0–1–2–3 that's 4 steps.
  • Answer depends on the graph, but the mechanism is fixed: the bitmask makes "visited all" a first-class part of the state. ✓ The n ≤ 20-ish size is what makes 2ⁿ masks affordable.

11Counting-by-Split DP (the Catalan family)

What it is: Count structures by where the defining split happens: unique BSTs (96 — each value takes a turn as root, left/right counts multiply), ways to parenthesize an expression (241 — each operator takes a turn as the outermost split). dp[n] = Σ dp[i] · dp[n-1-i] — the Catalan convolution.

Intuition: When a structure has a distinguished top element (root, outermost operator, last-matched bracket pair), condition on it: the two sides become independent smaller instances, so counts multiply across the split and add across split choices. Recognize the convolution shape and you get 96, valid parenthesizations, triangulations, and full binary trees from one recurrence — Catalan numbers by any costume.

Approach:

int numTrees(int n) {
    vector<long long> dp(n + 1, 0); dp[0] = 1;      // empty tree: one way
    for (int nodes = 1; nodes <= n; ++nodes)
        for (int left = 0; left < nodes; ++left)
            dp[nodes] += dp[left] * dp[nodes - 1 - left];
    return dp[n];
}

Challenges & pitfalls

dp[0] = 1 (the empty structure counts as one way — forgetting it zeroes everything); overflow before the modulus/answer bound (§0.6); why multiply (independence across the split — one sentence, expect the question); spotting Catalan in costume (the constraint n ≤ 19 with int answers is a hint).

LC 96 Unique Binary Search TreesLC 241 Different Ways to Add Parentheses
Example 1: Unique Binary Search Trees (LC 96) — how many structurally distinct BSTs hold values 1..n, for n = 3?
  • Condition on which value is the root: its smaller values form the left subtree, larger values the right, and those two counts are independent → multiply, then add over all root choices.
  • dp[0] = 1 (empty), dp[1] = 1, dp[2] = 2. For dp[3]: root 1 → dp[0]·dp[2] = 2; root 2 → dp[1]·dp[1] = 1; root 3 → dp[2]·dp[0] = 2. Sum = 5.
  • Answer 5 (the 3rd Catalan number). ✓ Multiplying left×right (independence) and summing over the root is the whole Catalan pattern.

12Digit DP (counting numbers with digit properties)

What it is: Count numbers in [0, N] whose digits satisfy a property (contain only allowed digits, no consecutive ones, count of digit '1'). Walk N's digits left to right; state = (position, tight — "am I still glued to N's prefix?", plus property-specific state). Rare but unmistakable — nothing else looks like it.

Intuition: Every number ≤ N either shares N's prefix up to position i then uses a smaller digit (after which all remaining positions are free — count them combinatorially/memoized), or equals N's prefix entirely. The tight flag captures exactly this: tight ⇒ the current digit is capped at N's digit; not tight ⇒ 0–9 free and the memo applies (free states don't depend on N — that's why the memo works). Add a started flag when leading zeros matter.

Approach (skeleton):

// count numbers in [0, N] whose digits all belong to set D  (902-style)
long long f(int pos, bool tight, string& N, vector<long long>& memo /* non-tight only */) {
    if (pos == (int)N.size()) return 1;
    if (!tight && memo[pos] != -1) return memo[pos];
    long long res = 0;
    int cap = tight ? N[pos] - '0' : 9;
    for (int d = 0; d <= cap; ++d) {
        if (!allowed(d)) continue;
        res += f(pos + 1, tight && d == cap, N, memo);
    }
    if (!tight) memo[pos] = res;
    return res;
}

Challenges & pitfalls

memoizing tight states (they depend on N's prefix — memo only the free ones); leading zeros (a "started" dimension when zeros interact with the property); shorter-length numbers (902 counts them separately with pure combinatorics — don't forget them); off-by-one on [0, N] vs [1, N]; this pattern is low-frequency — learn the skeleton, don't over-invest.

LC 902 Numbers At Most N Given Digit SetLC 233 Number of Digit OneLC 600 Non-negative Integers without Consecutive Ones
Example 1: Numbers At Most N Given Digit Set (LC 902) — allowed digits {1, 3, 5, 7}, N = 100. How many positive integers ≤ 100 use only those digits?
  • Count by length:
    • 1-digit numbers: each of the 4 digits works → 4 numbers (1, 3, 5, 7).
    • 2-digit numbers: both positions free (all 2-digit numbers from these digits are ≤ 99 < 100) → 4 × 4 = 16.
    • 3-digit numbers ≤ 100: the only 3-digit number ≤ 100 is 100 itself, whose digits 1,0,0 include 0 (not allowed) → 0.
  • Total = 4 + 16 = 20. ✓ Shorter-length numbers are pure combinatorics; the tight flag matters only when a number's length equals N's and it hugs N's prefix.

Summary Table — Recognition Cheat Sheet

#PatternStateTrigger phraseComplexity
1Grid paths(i, j)"grid, move right/down"O(mn)
2Grid twists(i, j), direction/shape twist"minimum initial…", "largest square"O(mn)
3Two-sequence(i, j) prefixes"edit distance", "common subsequence"O(mn)
4Wildcard/regex(i, j) + star branches"pattern with * and ?"O(mn)
5Palindrome (l, r)substring range"palindromic subsequence", "min insertions"O(n²)
6Interval DP(l, r), last event"burst", "cut", cost depends on neighborsO(n³)
7Multi-budget knapsack(i, b₁, b₂)"two constraints", "dice to target"O(states·choices)
8Augmented state machine(day, k, hold)"at most k transactions"O(nk)
9Game minimax(l, r) margin"two players play optimally"O(n²)
10Bitmask(mask[, last])n ≤ 20, "assign/visit all"O(2ⁿ·n)
11Counting by split(n) convolution"how many structures"O(n²)
12Digit DP(pos, tight, …)"numbers ≤ N with digit property"O(digits·states)
Study order: 1 → 3 (the two workhorse 2D shapes) → 5 → 2 → 7 → 8 (extensions of things you know from 1D) → 6 → 9 (the (l,r) pair — 6 is the hardest idea, 9 the cleanest trick) → 4 → 10 → 11 → 12 (specialists — recognize on sight, drill the skeletons).
The interview sentence (per your communication playbook §1.3): "For the subproblem to be self-contained the state needs ⟨dims⟩, so dp[⟨state⟩] = ⟨one sentence⟩. The transition partitions on ⟨the last decision⟩, dependencies point ⟨direction⟩ so I'll iterate ⟨order⟩ — that's O(states × transition) = ⟨X⟩, and I can compress ⟨dim⟩ since it only reads the previous layer."