Longest Common Subsequence – Solution & Complexity
Solution Walkthrough
Step 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.
- A subsequence preserves relative order, but it can skip characters, so
Step 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.
- Let
Step 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.
- The recursive version recomputes the same suffix pairs like
Step 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].
- Let
Step 5: Dry run on `text1 = "abcde"`, `text2 = "ace"`
We build a
(m + 1) x (n + 1)table where rows are prefixes oftext1and columns are prefixes oftext2. The first row and first column stay0because an empty string has LCS length0with 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 is3.Step 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]).
Step 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.
- If either string is empty, the answer is
Step 8: Final full solution and complexity
Use bottom-up DP over all prefix pairs. This solves
m * nsubproblems, each inO(1)time, so the total time isO(m*n). The full table usesO(m*n)space, and a rolling-row optimization can reduce that toO(min(m, n))when you only need the length.