Longest common subsequence
Given two strings text1 and text2, return the length of their longest common
subsequence: a string that can be obtained from each of them by deleting some characters without reordering the
rest. Return 0 if they share no character.
Examples
| Input | Output | Why |
|---|---|---|
text1 = "abcde", text2 = "ace" | 3 | "ace". |
text1 = "abc", text2 = "def" | 0 | Nothing in common. |
text1 = "ABCBDAB", text2 = "BDCABA" | 4 | For example "BCBA". |
Constraints
- 1 ≤ text1.length, text2.length ≤ 1000
- Lowercase or uppercase English letters
Hints
Hint 1
Let L[i][j] be the answer for the prefixes text1[:i] and text2[:j]. Empty prefixes give 0.
Hint 2
If the last characters match, L[i][j] = 1 + L[i − 1][j − 1].
Hint 3
Otherwise one of the two last characters is not used: L[i][j] = max(L[i − 1][j], L[i][j − 1]).
Where people lose marks
- Mixing prefix lengths and character indices: L[i][j] compares text1[i − 1] and text2[j − 1].
- On a match, taking max(L[i − 1][j], L[i][j − 1]) + 1, which can count one character twice.
- Confusing it with the longest common substring, which must be contiguous.
Dry run on "abcde","ace"
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 16
Base cases filled first.
-
Step 2 / 16
'a' matches: 1 + the diagonal = 1.
-
Step 3 / 16
'a' ≠ 'c': the larger of above and left, 1.
-
Step 4 / 16
'a' ≠ 'e': the larger of above and left, 1.
-
Step 5 / 16
'b' ≠ 'a': the larger of above and left, 1.
-
Step 6 / 16
'b' ≠ 'c': the larger of above and left, 1.
-
Step 7 / 16
'b' ≠ 'e': the larger of above and left, 1.
-
Step 8 / 16
'c' ≠ 'a': the larger of above and left, 1.
-
Step 9 / 16
'c' matches: 1 + the diagonal = 2.
-
Step 10 / 16
'c' ≠ 'e': the larger of above and left, 2.
-
Step 11 / 16
'd' ≠ 'a': the larger of above and left, 1.
-
Step 12 / 16
'd' ≠ 'c': the larger of above and left, 2.
-
Step 13 / 16
'd' ≠ 'e': the larger of above and left, 2.
-
Step 14 / 16
'e' ≠ 'a': the larger of above and left, 1.
-
Step 15 / 16
'e' ≠ 'c': the larger of above and left, 2.
-
Step 16 / 16
'e' matches: 1 + the diagonal = 3. The answer is in the corner: 3.
How to think about it
Index states by prefix lengths: L[i][j] is the LCS of text1[:i] and text2[:j],
with L[0][·] = L[·][0] = 0. If the last characters agree, an optimal common subsequence can end with that
character, giving 1 + L[i − 1][j − 1]. If they differ, at least one of them is unused, giving the larger of
L[i − 1][j] and L[i][j − 1]. Fill row by row; each row needs only the previous one.
Complexity. Time O(n · m) · Space O(m) with two rows
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def lcs_length(text1, text2):
m = len(text2)
prev = [0] * (m + 1) # row i - 1 of the table
for ch in text1:
cur = [0] * (m + 1)
for j in range(1, m + 1):
if ch == text2[j - 1]:
cur[j] = prev[j - 1] + 1 # match: extend the diagonal
else:
cur[j] = max(prev[j], cur[j - 1])
prev = cur
return prev[m]#include <algorithm>
#include <string>
#include <vector>
int solve(std::string text1, std::string text2) {
const int m = text2.size();
std::vector<int> prev(m + 1, 0), cur(m + 1, 0);
for (char ch : text1) {
for (int j = 1; j <= m; ++j)
cur[j] = ch == text2[j - 1] ? prev[j - 1] + 1 : std::max(prev[j], cur[j - 1]);
std::swap(prev, cur);
}
return prev[m];
}class Solution {
public int lcsLength(String text1, String text2) {
int m = text2.length();
int[] prev = new int[m + 1], cur = new int[m + 1];
for (char ch : text1.toCharArray()) {
for (int j = 1; j <= m; j++)
cur[j] = ch == text2.charAt(j - 1) ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]);
int[] t = prev; prev = cur; cur = t;
}
return prev[m];
}
}function lcsLength(text1, text2) {
const m = text2.length;
let prev = new Array(m + 1).fill(0);
for (const ch of text1) {
const cur = new Array(m + 1).fill(0);
for (let j = 1; j <= m; j++) cur[j] = ch === text2[j - 1] ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]);
prev = cur;
}
return prev[m];
}fn solve(text1: &str, text2: &str) -> i64 {
let b: Vec<char> = text2.chars().collect();
let m = b.len();
let mut prev = vec![0i64; m + 1];
for ch in text1.chars() {
let mut cur = vec![0i64; m + 1];
for j in 1..=m {
cur[j] = if ch == b[j - 1] { prev[j - 1] + 1 } else { prev[j].max(cur[j - 1]) };
}
prev = cur;
}
prev[m]
}solve :: String -> String -> Int
solve a b = last (foldl row (replicate (length b + 1) 0) a)
where
-- one row of the table from the previous row, for the character ch of the first string
row prev ch = scanl step 0 (zip3 b prev (tail prev))
where step left (c, diag, up) = if c == ch then diag + 1 else max up left
Variants to try
- Return one longest common subsequence by walking back from L[n][m].
- The fewest deletions to make the two strings equal: n + m − 2 · LCS.
- The shortest string containing both as subsequences: n + m − LCS.
Next problems
The cheapest way up a staircase
Each step has a cost; climb one or two steps at a time; start on step 0 or 1. Return the cheapest way past the top.
Open the problem → Medium15 minCan a subset reach the target?
Decide whether some subset of positive integers adds up to exactly the target. A boolean array over totals, filled downwards for each number.
Open the problem → Medium15 minCount the palindromic substrings
Count every contiguous substring that reads the same backwards, counting different positions separately. Each successful expansion step is one more.
Open the problem →Write a solution and run it against the real test table.
Keep going
The cheapest way up a staircase
Each step has a cost; climb one or two steps at a time; start on step 0 or 1. Return the cheapest way past the top.
Open the problem → Medium15 minCan a subset reach the target?
Decide whether some subset of positive integers adds up to exactly the target. A boolean array over totals, filled downwards for each number.
Open the problem → Medium15 minCount the palindromic substrings
Count every contiguous substring that reads the same backwards, counting different positions separately. Each successful expansion step is one more.
Open the problem → Dynamic programmingThe pattern behind it
Write the recursion, notice the repeats, fill a table in an order that makes each entry final.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.