Example 1
ready- Input
text1 = "abcde" text2 = "ace"- Expected
3- Why
- a, c, e stay in order in both strings
Two edited versions of a note may share a clear backbone even when words were inserted between its letters. You want the length of that backbone.
Given lowercase strings text1 and text2, return the length of their longest common
subsequence. A subsequence keeps characters in their original order but may skip
characters between them. A common subsequence must be obtainable from both strings.
Characters don't need to be adjacent. Their left-to-right order is the part you can't change.
1 <= text1.length, text2.length <= 500text1 and text2 contain only lowercase English letters.Hints
Compare one position from each string. If the characters match, you can keep that character and move both positions. What choices remain when they differ?
Let a state describe the answer for two suffixes, or for two prefixes. Only the two current boundaries matter; the exact characters you skipped earlier don't.
A bottom-up row depends only on the previous row and the cell immediately to its left. Preserve the old diagonal value before overwriting the current cell.
How would you reconstruct one longest common subsequence instead of returning only its length?
Visible cases
text1 = "abcde"
text2 = "ace"3text1 = "abc"
text2 = "abc"3text1 = "abc"
text2 = "def"0Interview signal
At positions i and j, matching characters belong together: count one and advance
both positions. When they differ, skip either text1[i] or text2[j] and keep the
better result.
A raw recursion repeats the same pair of positions. Dynamic programming (DP) stores
answers to overlapping smaller problems; memoization is the top-down form that
caches each (i, j) answer after computing it once.
from functools import cache
def longest_common_subsequence(text1: str, text2: str) -> int:
@cache
def solve(i: int, j: int) -> int:
if i == len(text1) or j == len(text2):
return 0
if text1[i] == text2[j]:
return 1 + solve(i + 1, j + 1)
return max(solve(i + 1, j), solve(i, j + 1))
return solve(0, 0)The recurrence examines the full choice space without recomputing a state. It also makes the correctness argument visible: a matching pair advances both strings, while a mismatch forces at least one current character to be absent from any chosen answer.
Time O(mn) because there are at most m · n position pairs. Space O(mn) for the
cache, plus a recursion stack of at most m + n calls.
The bottom-up LCS DP usually uses a grid where each cell stores the answer for two prefixes. Only the previous row is needed, so one row is enough if you preserve the old diagonal before overwriting it.
def longest_common_subsequence(text1: str, text2: str) -> int:
if len(text2) > len(text1):
text1, text2 = text2, text1
dp = [0] * (len(text2) + 1)
for char1 in text1:
diagonal = 0
for j, char2 in enumerate(text2, start=1):
above = dp[j]
if char1 == char2:
dp[j] = diagonal + 1
else:
dp[j] = max(dp[j], dp[j - 1])
diagonal = above
return dp[-1]Before an update, dp[j] is the cell above and diagonal is the previous row's
dp[j - 1]. After an update, dp[j - 1] is the cell to the left. Those are exactly the
three neighbors in the two-dimensional recurrence.
Time O(mn) because every pair of character positions is processed once. Space O(min(m, n)) because the shorter string controls the row width.
Ready to run.
3 cases are queued.
Visible testcase
Case 1
Expected
3