112. Longest Common Subsequence
View on NeetCode
View on LeetCode
Problem
Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.
A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.
- For example,
"ace"is a subsequence of"abcde".
A common subsequence of two strings is a subsequence that is common to both strings.
Example 1:
Input: text1 = "abcde", text2 = "ace"
Output: 3
Explanation: The longest common subsequence is "ace" and its length is 3.
Example 2:
Input: text1 = "abc", text2 = "abc"
Output: 3
Explanation: The longest common subsequence is "abc" and its length is 3.
Example 3:
Input: text1 = "abc", text2 = "def"
Output: 0
Explanation: There is no such common subsequence, so the result is 0.
Constraints:
1 <= text1.length, text2.length <= 1000text1andtext2consist of only lowercase English characters.
Solution
Approach: 2D Dynamic Programming
The key insight is that if characters match, we extend the LCS. If they don’t match, we take the maximum LCS from either skipping a character in text1 or text2.
Implementation
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
# Characters match - extend LCS
dp[i][j] = dp[i-1][j-1] + 1
else:
# Take max of skipping char in text1 or text2
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
Approach 2: Space-Optimized
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
# Use shorter string for space optimization
if m < n:
text1, text2 = text2, text1
m, n = n, m
prev = [0] * (n + 1)
for i in range(1, m + 1):
curr = [0] * (n + 1)
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev = curr
return prev[n]
Approach 3: Recursion with Memoization
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
memo = {}
def dp(i, j):
if i == len(text1) or j == len(text2):
return 0
if (i, j) in memo:
return memo[(i, j)]
if text1[i] == text2[j]:
result = 1 + dp(i + 1, j + 1)
else:
result = max(dp(i + 1, j), dp(i, j + 1))
memo[(i, j)] = result
return result
return dp(0, 0)
Complexity Analysis
2D DP:
- Time Complexity: O(m * n), where m and n are lengths of the two strings.
- Space Complexity: O(m * n) for the dp table.
Space-Optimized:
- Time Complexity: O(m * n).
- Space Complexity: O(min(m, n)), only storing two rows.
Key Insights
-
DP Table Meaning:
dp[i][j]= length of LCS of text1[0:i] and text2[0:j]. -
Character Match: If text1[i-1] == text2[j-1], we found a common character. Add 1 to LCS of previous positions.
- Character Mismatch: Take maximum of:
- LCS without current char from text1: dp[i-1][j]
- LCS without current char from text2: dp[i][j-1]
-
Base Cases: Empty string with any string has LCS of 0 (first row and column are 0).
-
Building Up: We build the solution from smaller subproblems (shorter prefixes).
- Subsequence vs Substring: Unlike substring, subsequence allows gaps, so we check all combinations.