Longest Common Subsequence – Solution & Complexity
Solution Walkthrough
1. Understand subsequences and why brute force fails
- A subsequence preserves relative order, but it can skip characters, so
"ace"is a subsequence of"abcde"while"aec"is not. - A brute-force idea is to generate every subsequence of one string and check whether it appears in the other. That quickly explodes to
2^mor2^npossibilities. - We need a way to reuse overlapping work across prefixes of both strings, which leads naturally to dynamic programming.
2. Start from the naive recursion
- Let
dfs(i, j)mean the LCS length usingtext1[i:]andtext2[j:]. - If the current characters match, they can contribute
1plus the best answer for the remaining suffixes. - Otherwise, we must try skipping one character from either string and keep the better result. This is correct, but without caching it revisits the same
(i, j)states many times.
3. Cache overlapping states with memoization
- The recursive version recomputes the same suffix pairs like
(i, j)again and again. - Memoization stores each solved state once, turning the exponential tree of calls into at most
m * ndistinct subproblems. - This top-down approach already reaches optimal time, but an iterative bottom-up table is often simpler to reason about and easier to visualize in interviews.
4. Build the bottom-up 2D DP table
- Let
dp[i][j]be the LCS length of the prefixestext1[:i]andtext2[:j]. - If
text1[i - 1] == text2[j - 1], then those matching characters extend the best answer for the smaller prefixes:dp[i][j] = dp[i - 1][j - 1] + 1. - Otherwise, the best subsequence must skip one side's current character, so
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]). - Filling the table row by row gives the final answer at
dp[m][n].
5. Dry run on `text1 = "abcde"`, `text2 = "ace"`
We build a (m + 1) x (n + 1) table where rows are prefixes of text1 and columns are prefixes of text2. The first row and first column stay 0 because an empty string has LCS length 0 with anything.
dp[i][j] | "" | a | c | e |
|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 |
| a | 0 | 1 | 1 | 1 |
| ab | 0 | 1 | 1 | 1 |
| abc | 0 | 1 | 2 | 2 |
| abcd | 0 | 1 | 2 | 2 |
| abcde | 0 | 1 | 2 | 3 |
The last cell is 3, so the longest common subsequence length is 3.
6. Common mistakes
- Mixing up subsequences and substrings: subsequences can skip characters, substrings must stay contiguous.
- Using
text1[i]andtext2[j]while fillingdp[i][j]for prefixes. In the table, the current characters aretext1[i - 1]andtext2[j - 1]. - Forgetting the extra zero-padded row and column, which causes off-by-one errors in the recurrence.
- On a mismatch, taking
dp[i - 1][j - 1]directly instead ofmax(dp[i - 1][j], dp[i][j - 1]).
7. Edge cases to test mentally
- If either string is empty, the answer is
0. - If the strings share no characters in order, the answer stays
0across the table. - If the strings are identical, the answer is the full string length.
- Repeated characters still need order-aware matching, so you cannot greedily pair the first equal letter you see without considering future matches.
8. Final full solution and complexity
Use bottom-up DP over all prefix pairs. This solves m * n subproblems, each in O(1) time, so the total time is O(m*n). The full table uses O(m*n) space, and a rolling-row optimization can reduce that to O(min(m, n)) when you only need the length.