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.
Examples
Example 1:
| |
Example 2:
| |
Example 3:
| |
Follow up
Can you get the longest common subsequence as well? Longest Common Subsequence LCS 2 - Get Subsequence
Solution
For arbitrary inputs, it is NP-hard. For constant inputs, DP gives the solution in polynomial time ex: O(n^k). Also, there can be more than one LCSs and the solution for that is exponential time ex: O(k^n).
Video explanation
Here is the video explaining below methods in detail. Please check it out:
Method 1 - Recursion
The LCS length is found by iteratively comparing characters from start, adding 1 for matches and recursively comparing remaining subsequences after character removal for mismatches.
Code
| |
| |
Complexity
- ⏰ Time complexity:
O(2^(m*n)) - 🧺 Space complexity:
O(m*n)
Method 2 - Top Down DP
As we see in the diagram above, that we have overlapping subproblems. We might use memoization to overcome overlapping subproblems.
Since there are two changing values, i.e. i and j in the recursive function longestCommonSubsequence, we might apply a two-dimensional array as a cache.
Code
| |
| |
Complexity
- ⏰ Time complexity:
O(m*n) - 🧺 Space complexity:
O(m*n)
Method 3 - Bottom Up DP
We will solve it in bottom-up DP and store the solution of the sub problems in a solution array.
Let dp[i+1][j+1] be the length of the longest common subsequence of string text1 & text2, when text1[i] and text2[j] are compared to each other. (0<=i<=m-1, 0<=j<=n-1)
When i OR j are 0, then we have empty string. That means the common length is 0.
| |
We can also fill the array from end, like we did in recursion solution. But we are coding from the start.
Code
| |
| |
Complexity
- ⏰ Time complexity:
O(m*n) - 🧺 Space complexity:
O(m*n)
Dry Run
This is how the DP array will look like for string “abcde” and “ace”:
| Ø | a | c | e | |
|---|---|---|---|---|
| Ø | 0 | 0 | 0 | 0 |
| a | 0 | 1 | 1 | 1 |
| b | 0 | 1 | 1 | 1 |
| c | 0 | 1 | 2 | 2 |
| d | 0 | 1 | 2 | 2 |
| e | 0 | 1 | 2 | 3 |
When filling in we have 2 cases
- if
(text1.charAt(i) == text2.charAt(j)), then we do1 + dp[i-1][j-1]:
For e.g. filing in i = 1, j = 1 (for char a in text1 and text2), marked in green, and prev value in orange:
$$
\begin{matrix}
. & Ø & a & c & e \\
Ø & \colorbox{orange} 0 & 0 & 0 & 0 \\
a & 0 & \colorbox{lightgreen} 1 & 1 & 1 \\
…
\end{matrix}
$$
- if
(text1.charAt(i) != text2.charAt(j))then we filling usingMath.max(dp[i - 1][j], dp[i][j - 1])
For eg. i = 1, j = 2 (marked green), it takes marked in orange.:
$$ \begin{matrix} . & Ø & a & c & e \\ Ø & 0 & 0 & \colorbox{orange} 0 & 0 \\ a & 0 & \colorbox{orange} 1 & \colorbox{lightgreen} 1 & 1 \\ … \end{matrix} $$
Method 4 - Space Optimized Bottom up DP
The key observation in previous approach is that each iteration only needs previous row values. We can leverage this by using just two rows (dp[2][n+1]) instead of the entire table (dp[m+1][n+1]), reducing memory usage significantly. See the updated implementation below.
- Instead of maintaining a table of size
(m+1) x (n+1), we reduce the space to 2 rows of size(n+1). - These 2 rows alternate between representing the “current row” and “previous row”:
- For the current DP row, use
dp[i % 2]. - For the previous DP row, use
dp[(i-1) % 2].
- For the current DP row, use
Approach
- Space Reduction:
- Instead of creating a 2D
dptable of size(m+1) x (n+1), create adparray of size2 x (n+1). - These two rows are alternated during the computation.
- Instead of creating a 2D
- Row Alternation:
- For each character in
text1(indexed byi), compute the results row by row. - Use
i % 2for the current row and(i-1) % 2for the previous row. This alternation allows us to reuse space while maintaining correctness.
- For each character in
- Character Match:
- If
text1[i-1] == text2[j-1], updatedp[i % 2][j] = dp[(i-1) % 2][j-1] + 1. Add1to the diagonal value since this character contributes to the LCS.
- If
- Character Mismatch:
- If
text1[i-1] != text2[j-1], take the maximum of the two possibilities:dp[(i-1) % 2][j](skipping the current character intext1).dp[i % 2][j-1](skipping the current character intext2).
- Update
dp[i % 2][j]to be the maximum of these values.
- If
- Final Result:
- After iterating through both strings, the LCS length is stored in
dp[m % 2][n].
- After iterating through both strings, the LCS length is stored in
Code
| |
Complexity
- ⏰ Time complexity:
O(m*n) - 🧺 Space complexity:
O(2*n) = O(n)