When a problem compares two strings (or arrays), the state is a pair of positions: i in the first and j in the second. At each step you look at the two current characters and either match them or move past one of them. Longest Common Subsequence (LCS) and Edit Distance are the two reference problems; most others are small changes to their transitions.
The idea#
Let solve(i, j) be the answer for the suffixes a[i:] and b[j:] (or the prefixes a[:i], b[:j]; both work). There are two cases.
- Characters match: move both pointers. Nothing needs fixing, so this is usually free (or worth +1 in LCS).
- Characters differ: try each allowed operation, each of which moves one or both pointers, and take the best.
This is correct because once you've decided what happens to a[i] and b[j], the rest of the problem is the same question on shorter suffixes. There are only len(a) * len(b) such pairs, so the cost is .
How to spot it#
Look for these signals in the question.
- Two strings or arrays and words such as "common", "transform", "delete to make equal", "interleave", "match a pattern".
- Constraints around
len ≤ 1000each, so is fine. - A single string compared against itself in reverse is often this pattern too (palindromic subsequence), but it's usually easier as interval DP.
Template#
Here is the top-down shape, written on indices. Each operation from the idea above becomes a different pointer move.
solve(i, j):
if i == length of a:
return the cost when a has run out
if j == length of b:
return the cost when b has run out
if a[i] matches b[j]:
return match value + solve(i + 1, j + 1)
return best over each allowed operation:
solve(i + 1, j) # move past a[i]
solve(i, j + 1) # move past b[j]
solve(i + 1, j + 1) # move past both
memoize solve on (i, j)
answer: solve(0, 0)
The template uses indices on purpose. Passing slices such as word1[:-1] into the cache works too, but every call copies a string and hashes it. That adds an factor. Indices make each state .
Pitfalls#
These mistakes cause the most wrong answers.
- Memoizing on string slices instead of indices.
- Getting the base cases backwards. Decide what one string running out means for the other: insert the rest, delete the rest, or fail.
- Skipping a run of matches in a
whileloop after the memo lookup, then caching under the advanced indices. It's still correct, but the state you looked up is never stored, so repeated calls redo the work. Prefer one match per call. - When counting, forgetting that a match can also be skipped. Both branches add up.
Problems in this pattern#
These problems use this pattern. Solve them before you open the hints below. Start with LCS and Edit Distance.
- 392. Is Subsequence
- 1143. Longest Common Subsequence
- 72. Edit Distance
- 583. Delete Operation for Two Strings
- 97. Interleaving String
- 115. Distinct Subsequences
- 10. Regular Expression Matching
- 44. Wildcard Matching
Hints#
Open a hint only after you have tried the problem for a while.
Hint: Longest Common Subsequence
On a match: 1 + solve(i+1, j+1). On a mismatch: max(solve(i+1, j), solve(i, j+1)). Either string running out gives 0.
Hint: Edit Distance
On a match, move both for free. On a mismatch, take 1 + min of delete (i+1, j), insert (i, j+1) and replace (i+1, j+1). When a runs out, insert the rest of b: len(b) - j. When b runs out, delete the rest of a. Check: horse → ros is 3, intention → execution is 5.
Hint: Delete Operation for Two Strings
On a match: solve(i+1, j+1). On a mismatch: 1 + min(solve(i+1, j), solve(i, j+1)). There is also a one-line shortcut: keep the LCS and delete everything else, so the answer is len(a) + len(b) - 2 * LCS.
Hint: Interleaving String
can(i, j) is true if s1[:i] and s2[:j] can interleave to form s3[:i+j]. The next character of s3 must come from s1[i] or s2[j]. The position in s3 is i + j, so the state needs only i and j.
Hint: Distinct Subsequences
On a match there are two branches, and both add up: use it, solve(i+1, j+1), or skip it, solve(i+1, j). On a mismatch, only skip: solve(i+1, j).
Hint: Regular Expression Matching
The pattern is the second string, and . matches any single character. A * applies to the element before it, so check p[j+1] == '*' before matching p[j]. Then choose: match zero (skip the pair) or consume one text character and stay on the same pattern position.
Hint: Wildcard Matching
? matches any single character. A * gives a choice: match zero characters (skip the star) or consume one character of the text and stay on the star.
Hint: Is Subsequence
Two pointers solve it in one pass. Then think of it as the two-string table: dp[i][j] says whether s[:i] is a subsequence of t[:j], which is the shape the harder problems in this note use.
Key takeaways#
- State = one index into each string.
- Match: move both. Mismatch: try each operation as a pointer move.
- Base cases handle one string running out.
- Use indices, not slices, as cache keys.
- Many problems reduce to LCS with a formula on top.