2-D dynamic programming is the same discipline as 1-D — cache repeated subproblems instead of re-deriving them — with one extra dimension in the state. That extra dimension almost always comes from one of two places: a position in a grid (row, column), or a position in two strings at once (how far through the first, how far through the second). Recognizing which shape you're in tells you what the table's axes actually mean.
Grid DP: two ways in, one recurrence
Unique Paths is the cleanest version of grid DP: a robot moving only right or down means every cell has exactly two possible predecessors (above, or to the left), so dp[i][j] = dp[i-1][j] + dp[i][j-1] falls out immediately. Minimum Path Sum and Unique Paths II are the same recurrence with the combining operator swapped (min instead of +) or a hard constraint bolted on (an obstacle forces dp[i][j] = 0, which then naturally blocks anything trying to route through it). Maximal Square uses three predecessors instead of two — up, left, and diagonal — because extending a square by one row and one column needs all three neighboring squares to already support that larger size.
Before writing any 2-D recurrence, ask: does dp[i][j] represent "one position in a grid" or "how far through each of two strings"? The grid version almost always looks up, left, and sometimes diagonal. The two-string version almost always looks at dp[i-1][j], dp[i][j-1], and dp[i-1][j-1] — one step in string one, one step in string two, or one step in both at once.
Two-string DP: match, skip, or both
Longest Common Subsequence is the foundational version: if the two current characters match, they must both belong to the answer (dp[i-1][j-1] + 1); if they don't, try dropping either character and keep the better result. Edit Distance generalizes this to three possible operations instead of "match or skip." Distinct Subsequences flips it from an optimization into a count — and the recurrence becomes an addition, not a max, because "don't use this character" and "use this character to match" are both always simultaneously valid contributions, not competing choices.
The one-line difference that changes the entire problem: LCS vs. Longest Common Substring
Longest Common Subsequence and Longest Common Substring have recurrences that look almost identical — until a mismatch. LCS carries the best answer forward (max(dp[i-1][j], dp[i][j-1])), because a subsequence is allowed to skip characters and keep going. A substring is contiguous, so a mismatch doesn't just fail to extend the run — it endsdp[i][j] resets to 0. Mixing these two rules up (using the LCS carry-forward for a substring problem, or the substring reset for a subsequence problem) produces a plausible-looking wrong answer, not a crash — which is exactly why stating what dp[i][j] means in plain English first catches this before it's written.
Interval DP: substrings of a single string
Longest Palindromic Substring and Palindromic Substrings both use a different axis pairing: dp[i][j] means "is s[i..j] a palindrome," over a single string. The fill order here is the detail most likely to trip you up: checking a length-L interval depends on the already-computed answer for the interval two characters shorter inside it, so the table must be filled by increasing interval length — not row-by-row or column-by-column, which is everyone's first instinct coming from grid DP.
When a choice doesn't commit: Interleaving String and Wildcard Matching
Interleaving String's *-free version and Wildcard Matching's * both share a shape: at some point, more than one action could be valid right now, and only checking every possibility (rather than greedily locking in one) guarantees finding a path that actually works out. Interleaving String's ambiguity is "which source contributes this next matching character" — a greedy preference for one source can walk into a dead end that backtracking would have avoided. Wildcard Matching's * is explicitly two possibilities at once (match zero more characters, or match one more and stay available) — encoded directly as an || in the recurrence rather than a single choice.
Common mistakes
- Confusing subsequence and substring recurrences — carrying the best-so-far forward on a mismatch (correct for LCS) versus resetting to 0 (correct for a contiguous match) are opposite rules for superficially similar problems.
- Filling an interval-DP table in the wrong order — row-by-row or column-by-column reads an entry (a shorter interior interval) that hasn't been computed yet; interval problems need increasing-length order specifically.
- Using max where the problem needs a sum (or vice versa) — Distinct Subsequences needs both "skip this character" and "use this character" counts added together, since both are always valid at once; treating them as competing alternatives (taking a max) undercounts.
- Committing to one branch too early — Interleaving String and Wildcard Matching both need every valid possibility considered at an ambiguous point, not just whichever one happens to match first.
Takeaways
- 2-D DP's extra dimension is either "a grid position" (looks up/left/diagonal) or "how far through two strings" (looks at i-1,j / i,j-1 / i-1,j-1) — know which one you're in before writing the recurrence.
- State what dp[i][j] means in one plain-English sentence before coding it — that sentence is what catches an LCS-vs-substring mixup before it ships as a silently wrong answer.
- Interval DP (single-string problems like palindrome checks) must be filled by increasing interval length, not row-by-row.
- When a recurrence needs to add two contributions instead of choosing the better one, it's usually because both are simultaneously valid — not alternatives being compared.
Try it: the foundational grid recurrence
Unique Paths is the cleanest version of grid DP — get the up/left recurrence automatic here, since Minimum Path Sum and Unique Paths II are the same shape with small twists.
Loading starter code…
Try it: the foundational two-string recurrence
Longest Common Subsequence — do this one right before Longest Common Substring, so the carry-forward vs. reset-on-mismatch distinction is fresh.
Loading starter code…
Try it: interval DP
Longest Palindromic Substring — the increasing-length fill order here is worth sitting with; it's the detail everyone gets wrong the first time coming from grid DP.
Loading starter code…
That's 3 of the 12 problems in this topic. Longest Common Substring is worth doing right after LCS — the one-line reset-vs-carry-forward difference between them is the clearest example in this whole course of how a nearly-identical recurrence can silently answer a different question. See the full 2-D Dynamic Programming set →
Checkpoint · 2-D Dynamic Programming
5 questions · pass at 70%
Finished 2-D Dynamic Programming?
Pass the quiz to complete it automatically.