Problem
Given two strings, find the length of the longest subsequence that appears in both โ a subsequence keeps characters in their original order but doesn't require them to be contiguous. For example, "ace" is a valid subsequence of "abcde" because a, c, and e appear left-to-right with possible gaps.
- Input:
text1 = "abcde",text2 = "ace" - Output:
3 - Explanation: The longest common subsequence is "ace", which has length 3.
Counter-example: text1 = "abc", text2 = "def" โ output 0 because no characters are shared.
Intuition
When two characters match, we can extend whatever LCS existed before those positions. When they don't match, we try skipping one character from either string and take the better result. This optimal substructure โ "the LCS up to position i, j depends only on earlier sub-problems" โ is what makes bottom-up DP efficient here.
Approach 1 โ 2D Dynamic Programming
Build a 2D table where dp[i][j] stores the LCS length for text1[:i] and text2[:j]. Fill it row by row: either extend a match diagonally or inherit the best result from skipping one character.
- Create a
(len(text1)+1) ร (len(text2)+1)table of zeros โ the zero border represents the empty-string base case. - For each character pair
(text1[i-1], text2[j-1]):- If they match:
dp[i][j] = 1 + dp[i-1][j-1]. - If they don't:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
- If they match:
- Return
dp[len(text1)][len(text2)].
1def longestCommonSubsequence(text1: str, text2: str) -> int:
2 rows, cols = len(text1), len(text2)
3 dp = [[0] * (cols + 1) for _ in range(rows + 1)]
4
5 for i in range(1, rows + 1):
6 for j in range(1, cols + 1):
7 if text1[i - 1] == text2[j - 1]:
8 dp[i][j] = 1 + dp[i - 1][j - 1] # characters match, extend the prior LCS
9 else:
10 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # skip one character, keep the best
11
12 return dp[rows][cols]Time: O(m ร n) โ every cell in the table is filled exactly once.
Space: O(m ร n) โ the full table is held in memory throughout.
Approach 2 โ Space-Optimized Rolling Array
Each row only reads from the row directly above and the cell to its left, so we can discard older rows and keep only two at a time.
- Swap
text1andtext2iftext2is longer, so the shorter string determines the array length. - Keep
prev_row(the row above) and fill a freshcurr_rowfor each character intext1. - After processing each character, set
prev_row = curr_row. - Return
prev_row[len(text2)].
1def longestCommonSubsequence(text1: str, text2: str) -> int:
2 if len(text2) > len(text1):
3 text1, text2 = text2, text1 # keep the shorter string as text2 to minimize array size
4
5 prev_row = [0] * (len(text2) + 1)
6
7 for char1 in text1:
8 curr_row = [0] * (len(text2) + 1)
9 for j, char2 in enumerate(text2, 1):
10 if char1 == char2:
11 curr_row[j] = 1 + prev_row[j - 1] # diagonal: both characters contributed
12 else:
13 curr_row[j] = max(prev_row[j], curr_row[j - 1])
14 prev_row = curr_row
15
16 return prev_row[len(text2)]Time: O(m ร n) โ same computation, just without retaining old rows.
Space: O(min(m, n)) โ only two arrays of length min(m, n) + 1 are live at once.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| 2D DP | O(m ร n) | O(m ร n) | When you need to reconstruct the actual subsequence, not just its length |
| Rolling Array | O(m ร n) | O(min(m, n)) | When memory is constrained and you only need the length |
Common Mistakes
- Off-by-one between the table and the strings โ
dp[i][j]covers characters up to indexiandj, so the comparison usestext1[i-1]andtext2[j-1], nottext1[i]. - Only skipping in one direction when characters don't match โ returning just
dp[i-1][j]misses the path where you skip fromtext2; you must takemax(dp[i-1][j], dp[i][j-1]). - Confusing subsequence with substring โ "ace" is a valid common subsequence of "abcde" even though the characters aren't adjacent; the DP correctly allows gaps between matched characters.
- Overwriting the diagonal in a single-array in-place update โ if you update left-to-right in place,
dp[j-1]is already overwritten before it's needed as the diagonal for the current match; the two-array approach sidesteps this entirely. - Forgetting to swap so text2 is the shorter string โ in the rolling-array version the array length equals
len(text2), so failing to swap wastes memory proportional to the length difference.
Related Problems
edit-distanceโ uses the same 2D DP structure over two strings, extending it to count insertions, deletions, and substitutionslongest-increasing-subsequenceโ same subsequence concept applied to a single sequence with a numeric ordering constraintinterleaving-stringโ 2D DP over two strings checking a structural property rather than matching characterspalindromic-substringsโ DP over substrings that shares the "extend from a sub-problem" recurrence patternmaximal-squareโ 2D grid DP where each cell depends on its neighbors, the same dependency shape as LCS