Subsequence DP
A subsequence keeps the original order but does not have to be contiguous. That single condition decides how every state in this chapter is defined. One sequence gives you a one-dimensional table (LIS). Two sequences give you a two-dimensional table, where each cell answers a question about one pair of prefixes. LCS, edit distance, and the palindrome problems are that same table with a different rule in the mismatch case.
First: subsequence (may skip) vs subarray (must be contiguous)
This one condition decides how the state is defined, and what happens to a cell when the two characters do not match.
Every problem in this chapter turns on two words. Fix them first.
- A subsequence is what you get by taking some elements out of a sequence in their original order, with gaps allowed. In "abcde", "ace" is a subsequence.
- A subarray (called a substring when the sequence is a string) is a contiguous block of the sequence. In "abcde", "bcd" is a substring; "ace" is not.
Why does this matter before any code is written? Because it decides the shape of the transition. A contiguous problem must reset a cell to 0 the moment the two current elements differ, since the run is broken (LC 718). A subsequence problem may skip the mismatching element and keep the best result found so far (LC 1143). Same two-dimensional table, one different rule. Try it below.
Because elements may be skipped, "do not use this element" is always a legal move. So a mismatch carries the better of the smaller states forward with max, and never resets. Examples: LIS, LCS, edit distance.
A run has a definite last element, and one mismatch destroys the whole run. So a mismatch resets the cell to 0, and the answer is the maximum over the whole table. Examples: LC 718, maximum subarray sum (LC 53).
The most common mistake here
Read the problem and ask one question first: does it need the order kept, or does it need the elements next to each other? "Longest common subsequence" and "longest common subarray" differ by one word, and their transitions are not the same (sections 03 and 04 put the pair side by side). Misreading the question means defining the wrong state, and everything after that is wasted work.
Worked example A · longest increasing subsequence
MEDIUMLC 300 — subsequence DP on a single sequence, then the same answer in O(n log n).
The problem: given an array, find the length of the longest strictly increasing subsequence. Gaps are allowed. For [1, 3, 2, 4, 5] the answer is 4, from [1, 3, 4, 5] or [1, 2, 4, 5].
Brute force: each element is either taken or not, so there are 2ⁿ subsequences to check. At n = 40 that is already out of reach. Why DP applies: the problem has optimal substructure — remove the last element of a long increasing subsequence and what remains is still an increasing subsequence. So the real question is how to define the state.
Here is the central technique of subsequence DP: anchor the subproblem by its last element. Define dp[i] as the length of the longest increasing subsequence that ends at index i. Why insist on "ends at i"? Because only a fixed last element lets you ask whether one subsequence can be extended by another: look left for every j with nums[j] < nums[i], take the largest dp[j], and add 1. The base case is dp[i] = 1, since the element alone is already a subsequence. Step through it:
max(dp) reads the answer in one line. It raises an error on an empty list, which is safe here because LC 300 guarantees n ≥ 1.Time O(n²), space O(n). LC 300 can be solved faster with a greedy idea plus binary search, known as patience sorting. Keep an array tails in which tails[k] is the smallest possible last value among all increasing subsequences of length k+1 seen so far. For each new value, use binary search (the lower-bound template from Chapter 03) to find the first entry that is ≥ it and replace that entry. If every entry is smaller, append instead. tails stays sorted, and its length is the answer:
bisect_left does the binary search in one line. For the longest non-decreasing subsequence, where equal values are allowed, switch it to bisect_right.tails holds a length, not a subsequence
The length of tails is the correct answer, but the values inside it are not a valid subsequence of the input in general. A later replacement can overwrite an entry with a value that appears earlier in the array than the entries to its left. For nums = [3, 4, 1], tails ends as [1, 4], yet 1 comes after 4 in the input. If you also need to reconstruct one actual longest subsequence, record the position each value was placed at and follow those links backwards, or use the O(n²) version, where the predecessor of every cell is available directly.
The inner loop that scans everything to the left is replaced by one binary search: n operations, O(log n) each. At n = 10⁵, O(n²) is about 10 billion steps and the binary-search version is under two million.
Keep cnt[i] next to dp[i]: when a longer subsequence is found, reset the count; when the same length is matched, add to it. A main array plus a counting array is the standard shape of a counting DP.
dp[i] is about subsequences that end at i, and the longest one does not have to end at the last element. Take the maximum over the whole dp array.
The name "patience sorting" comes from a card game
The technique is named after the card game patience, called solitaire in the United States. Deal the cards one at a time and put each card on the leftmost pile whose top card is not smaller than it; if there is no such pile, start a new pile on the right. When the deck is finished, the number of piles equals the length of the longest increasing subsequence. The top cards of the piles, read from left to right, are exactly the tails array in the code above.
In practice: diff tools are built on subsequence DP
git diff and text comparison tools look for the longest sequence of lines that both files share in the same order. That is the longest common subsequence of the next section, with whole lines instead of characters. Real diff tools do not fill a plain m×n table. They use refinements such as the Myers algorithm, or Hunt–Szymanski, which turns the LCS problem into an LIS problem and is fast when the two files have few identical lines in common. The question they answer, though, is the one in the next section.
The contiguous case: maximum length of repeated subarray
MEDIUMLC 718 — the first two-dimensional table in this chapter, and the rule that a mismatch resets a cell.
The problem: given two integer arrays A and B, find the length of the longest contiguous block that appears in both. It asks for a subarray, so the elements must be next to each other, and that is what fixes the state.
Brute force: pick a start in A, pick a start in B, and compare forwards, which costs O(m·n·min(m, n)). The DP: the standard opening for a two-sequence problem is a two-dimensional table with A along the rows and B along the columns. Because the block has to be contiguous, the state must pin down where the block ends: dp[i][j] = the length of the longest common run ending exactly at A[i-1] and B[j-1]. Equal elements extend the run on the diagonal by one. Unequal elements break the run, so the cell goes back to 0:
The zeros carry the information
The most informative cells in this table are not the 1s, 2s and 3s. They are the zeros. Each 0 says "a run cannot end here". The diagonal line of 1 → 2 → 3 is one common run growing. In the next section the same pair of sequences is used with the contiguity requirement removed, and those zeros disappear. That is the difference between LC 718 and LCS, seen directly in the table.
Worked example B · longest common subsequence
MEDIUMLC 1143 — the central table of this chapter, and where the diagonal transition is explained in full.
The problem: given two strings, find the length of their longest common subsequence: shared, in the same order, gaps allowed. For "abcde" and "ace" the answer is 3, from "ace".
The DP: the same opening as LC 718 — a two-dimensional table with one string along the rows and the other along the columns. But because the result does not have to be contiguous, the state can be simpler: dp[i][j] = the length of the longest common subsequence of the first i characters of A and the first j characters of B. Nothing is said about where it ends. The transition only looks at the two last characters:
- Equal → matching this pair is never worse than leaving it out, so use it and add one to the answer for the two shorter prefixes:
dp[i][j] = dp[i-1][j-1] + 1. The diagonal is the only cell that means "both characters consumed as one matched pair". - Different → they cannot be a matched pair, so at least one of them is unusable here. Drop A's last character and you are left with dp[i-1][j]; drop B's and you are left with dp[i][j-1]. One of the two must be optimal, so keep the larger:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])— never a reset.
Base case: row 0 and column 0 are 0, because an empty string shares nothing. Fill the table one cell at a time and watch the blue dashed source cells change between a match and a mismatch:
dp[i][j] is about s[i-1] and t[j-1].LC 718 and LC 1143 side by side. One word in the problem statement, and everything below the third row changes:
| Aspect | LC 718 · subarray | LC 1143 · subsequence |
|---|---|---|
| Contiguous? | Yes (a subarray) | No (a subsequence) |
| What dp[i][j] means | The common run that ends at A[i-1] and B[j-1] | The LCS of the first i and the first j characters |
| On a match | dp[i-1][j-1] + 1 (diagonal) | dp[i-1][j-1] + 1 (diagonal) |
| On a mismatch | Reset to 0 (the run is broken) | max(above, left) (skip one character) |
| Where the answer is | The maximum of the whole table | The bottom-right cell, dp[m][n] |
Complexity, and the follow-up questions
There are (m+1)(n+1) states and each transition is O(1), so the time is O(mn) and the table itself is O(mn) space. The space can be reduced to O(min(m, n)) by keeping only one row, with the shorter string along the columns — but not for free. The transition also reads the diagonal dp[i-1][j-1], which a left-to-right pass overwrites before it is used, so that value must be saved in a temporary variable before the cell is assigned. Common follow-ups: (1) print the subsequence itself → walk backwards from the bottom-right cell; (2) what is LC 1035, uncrossed lines? → the same problem in different words, with no change to the code (see the problem set); (3) how does it differ from LC 718? → the mismatch row of the table above.
Worked example C · edit distance
HARDLC 72 — one cell, three source cells, and each one is a different operation.
The problem: turn word1 into word2. Each step may insert, delete, or replace one character. Find the fewest steps. For "horse" → "ros" the answer is 3.
The DP: the same table again. dp[i][j] = the fewest operations that turn the first i characters of word1 into the first j characters of word2. What is new is that a mismatch has three source cells instead of two. Stand on dp[i][j] and ask what the last operation was:
- Replace: change the i-th character of word1 into the j-th character of word2. Both last characters are consumed at once, which leaves
dp[i-1][j-1]— the cell on the diagonal. - Delete: remove the i-th character of word1. word1 gets one shorter and word2 is untouched, which leaves
dp[i-1][j]— the cell above. - Insert: append the j-th character of word2 to word1. That character is now matched, so one more character of word2 is handled and word1 is otherwise unchanged, which leaves
dp[i][j-1]— the cell on the left.
When the two last characters are already equal, no operation is needed: copy dp[i-1][j-1] without adding anything. Watch the three sources light up together on every mismatch:
min takes all three arguments at once, so the three operations fit on one expression. The three source cells are the diagonal, the one above, and the one on the left, matching the three highlighted cells in the animation.In practice: spell checking and DNA alignment use this table
Edit distance, also called Levenshtein distance, is a general measure of how similar two sequences are. Search boxes and input methods use it to rank "did you mean …" suggestions. In bioinformatics, aligning DNA or protein sequences (the Needleman–Wunsch algorithm) is the same table with a score for each operation instead of a fixed cost of 1. Line-level git diffs, fuzzy finders such as fzf, and plagiarism checks all rest on this insert-delete-replace DP.
Palindromes: expand outwards, or shrink inwards
MEDIUMLC 5 / 516 / 647 / 132 — the last piece of subsequence DP, and the entry point to interval DP.
A palindrome reads the same forwards and backwards. There are two complementary ways to work with one:
A palindrome is symmetric about its center, so try all 2n−1 centers: n single characters for odd lengths and n−1 gaps between neighboring characters for even lengths. From each center, move outwards while the two ends match. O(n²) time, O(1) extra space. Best for substring problems, which need contiguity: LC 5, LC 647.
dp[i][j] states something about the substring s[i..j]: whether it is a palindrome, or how long its longest palindromic subsequence is. The transition compares the two ends s[i] and s[j] and reads the shorter interval inside it, dp[i+1][j-1]. That dependency forces the iteration order: fill by increasing interval length, or let i run from high to low so that row i+1 is finished first. This is the entry point to interval DP in Chapter 10, and it is what LC 516 needs.
Start with expanding from a center, which LC 5 (longest palindromic substring) and LC 647 (counting palindromic substrings) both use:
s[l+1:r] is half-open on the right, which is exactly the palindrome range, so the length calculation disappears. Python slicing absorbs the off-by-one.Now LC 516, the longest palindromic subsequence, from the interval DP side. There is a shortcut worth knowing: the longest palindromic subsequence of s is the longest common subsequence of s and reverse(s), so the code from the previous section solves it unchanged. Writing the interval DP directly also works, and it shows the iteration order that Chapter 10 is built on: i runs from high to low, j from low to high, because dp[i][j] reads dp[i+1][…].
return LCS(s, s[::-1]), reusing LC 1143. A palindromic subsequence of s is a subsequence shared by s and its reverse.Going further · LC 132, palindrome partitioning II
What happens when palindromes meet partitioning: cut the string into pieces so that every piece is a palindrome, using the fewest cuts. The approach is two DPs stacked. First, interval DP builds isPal[i][j], a table that answers "is s[i..j] a palindrome" in O(1). Then a one-dimensional DP over the cuts: dp[i] = min(dp[j] + 1) over every j for which s[j..i-1] is a palindrome. Building a lookup table first and running the main DP on top of it is a common combination, and worth practising once the main line feels stable.
Problem set: 12 subsequence DP problems
Core setGrouped as subsequence checking, LIS, contiguous, two sequences, and palindromes, from easier to harder. Think for 30 seconds before opening the hint.
Chapter quiz
✎ QuizAnswer all 7 correctly to mark this chapter as complete.
What is the difference between a subsequence and a subarray (a substring)?
In the O(n²) solution to LC 300, dp[i] is defined as the length of the longest increasing subsequence ending at index i, rather than the longest one among the first i elements. Why?
Work it out by hand: for nums = [1, 3, 2, 4, 5], how long is the longest increasing subsequence?
Both LC 718 (maximum length of repeated subarray) and LC 1143 (longest common subsequence) fill a two-dimensional table. How do they differ when the two current characters are not equal?
Which of these statements about dp[i][j] in LC 1143 (longest common subsequence) are correct? (Select all that apply)
In LC 72 (edit distance), when word1[i-1] != word2[j-1] the transition is dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1. Which operation does each source cell stand for?
LC 516 asks for the longest palindromic subsequence. Which known problem can it be turned into directly?
- The first fork: a subsequence may skip elements, a subarray or substring may not. Decide which one the problem wants before defining the state, because it decides whether a mismatch resets the cell to 0 or keeps the larger neighbor.
- On a single sequence (LIS), anchor the subproblem by its last element: dp[i] is the length of the longest increasing subsequence ending at index i. The price of that choice is that the answer is the maximum of the whole array, not dp[n-1].
- LIS drops from O(n²) to O(n log n) with tails, where tails[k] is the smallest possible last value of an increasing subsequence of length k+1, plus a binary search. Its length is the answer; its contents are not a subsequence of the input.
- The skeleton for two sequences: one table, one string along the rows and the other along the columns. A match always goes to the diagonal, dp[i-1][j-1]. All the differences between these problems are in the mismatch case.
- The pair to remember: LC 718 resets to 0 on a mismatch (contiguous), LC 1143 takes max(above, left) (not contiguous), and LC 72 has one more source (diagonal = replace, above = delete, left = insert, take the smallest and add one).
- Two views on palindromes: expand from a center (outwards, O(1) extra space, for substrings — LC 5, LC 647) and interval DP (inwards, dp[i][j], for subsequences — LC 516). Also, the longest palindromic subsequence of s is LCS(s, reverse(s)).
- The iteration order comes from the transition. Interval DP reads dp[i+1][j-1], so i must run downwards or the table must be filled by increasing interval length. Complexity is (number of states) × (work per transition), and the table counts in the space. A rolling row is safe only when the transition reads the previous row alone; if it also reads the diagonal, save that value before overwriting the cell.
- The most common bug in two-sequence DP is off-by-one indexing. The table has one extra row and column, so dp[i][j] is about s[i-1] and t[j-1], and the empty-string row and column must be filled first.