Skip to content

1143. Longest Common Subsequence (Medium)

Problem

Given two strings text1 and text2, return the length of their longest common subsequence (LCS). A subsequence is a sequence that can be derived from another by deleting zero or more elements without changing relative order.

Example

  • text1 = "abcde", text2 = "ace"3 ("ace")
  • text1 = "abc", text2 = "abc"3
  • text1 = "abc", text2 = "def"0

LeetCode 1143 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Recursive

f(i, j) = LCS of text1[i:] and text2[j:].

def longest_common_subsequence(text1, text2):
def f(i, j):
if i == len(text1) or j == len(text2): # L1: O(1) base case
return 0
if text1[i] == text2[j]: # L2: O(1) char match
return 1 + f(i + 1, j + 1) # L3: one recursive call (diagonal)
return max(f(i + 1, j), f(i, j + 1)) # L4: two recursive calls
return f(0, 0)

Where the time goes, line by line

Variables: m = len(text1), n = len(text2).

LinePer-call costTimes executedContribution
L1-L2 (base + match check)O(1)O(1)per callO(1)O(1) each
L3 (one call on match)O(1)O(1) work + 1 callper matchincluded below
L4 (two calls on mismatch)O(1)O(1) work + 2 callsworst case every callO(2m+n)O(2^{m+n}) ← dominates

Every mismatch doubles the call tree. Without memoization the same (i, j) is recomputed at every overlapping node.

Complexity

  • Time: O(2m+n)O(2^{m+n}), driven by L4 double-branching on mismatches.
  • Space: O(m+n)O(m + n) recursion depth.

Approach 2: 2-D bottom-up DP (canonical)

dp[i][j] = LCS of text1[:i] and text2[:j]. dp[i][j] depends on dp[i-1][j-1], dp[i-1][j], dp[i][j-1].

def longest_common_subsequence(text1, text2):
m, n = len(text1), len(text2) # L1: O(1)
dp = [[0] * (n + 1) for _ in range(m + 1)] # L2: O(m*n) table init (zeros = base cases)
for i in range(1, m + 1): # L3: outer loop O(m)
for j in range(1, n + 1): # L4: inner loop O(n)
if text1[i - 1] == text2[j - 1]: # L5: O(1) char match
dp[i][j] = 1 + dp[i - 1][j - 1] # L6: O(1) diagonal + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # L7: O(1) max of up/left
return dp[m][n] # L8: O(1) answer

Where the time goes, line by line

Variables: m = len(text1), n = len(text2).

LinePer-call costTimes executedContribution
L1-L2 (init)O(1)O(1) or O(mn)O(m·n)1O(mn)O(m · n)
L3+L4 (double loop)O(1)O(1) bodym · nO(mn)O(m · n) ← dominates
L5-L7 (cell fill)O(1)O(1)once per cellincluded above

Each cell fill is a single comparison plus one or two table lookups, all O(1)O(1). The double loop at L3/L4 drives the total.

Complexity

  • Time: O(mn)O(m · n), driven by L3/L4 (the full double loop).
  • Space: O(mn)O(m · n) for the DP table.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: 1-D rolling array (optimal space)

Keep two rows, current and previous. Or one row with a diagonal scratch scalar.

def longest_common_subsequence(text1, text2):
m, n = len(text1), len(text2) # L1: O(1)
if m < n: # L2: O(1) ensure text1 is longer (minimize space)
text1, text2 = text2, text1
m, n = n, m
prev = [0] * (n + 1) # L3: O(n) init previous row
for i in range(1, m + 1): # L4: outer loop O(m)
curr = [0] * (n + 1) # L5: O(n) allocate current row
for j in range(1, n + 1): # L6: inner loop O(n)
if text1[i - 1] == text2[j - 1]: # L7: O(1) char match
curr[j] = 1 + prev[j - 1] # L8: O(1) diagonal + 1
else:
curr[j] = max(prev[j], curr[j - 1]) # L9: O(1) max of up/left
prev = curr # L10: O(1) roll the row
return prev[n] # L11: O(1) answer

Where the time goes, line by line

Variables: m = len(text1), n = len(text2) (after the swap so m >= n).

LinePer-call costTimes executedContribution
L1-L3 (init)O(n)O(n)1O(n)O(n)
L5 (row alloc)O(n)O(n)mO(mn)O(m · n)
L4+L6 (double loop)O(1)O(1) bodym · nO(mn)O(m · n) ← dominates
L7-L9 (cell fill)O(1)O(1)once per cellincluded above

Same time as 2-D DP but space is O(n)O(n) (two rows of size n). The swap at L2 ensures we allocate the shorter dimension.

Complexity

  • Time: O(mn)O(m · n), driven by L4/L6 (the double loop).
  • Space: O(min(m,n)O(min(m, n)) for the two rolling rows.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
RecursiveO(2m+n)O(2^{m+n})O(m+n)O(m+n)
2-D DPO(mn)O(m · n)O(mn)O(m · n)
1-D rolling DPO(mn)O(m · n)O(min(m,n)O(min(m, n))

LCS is the prototype of the “compare two sequences” DP. Its recurrence pattern, if match then diagonal+1 else max(up, left), shows up in Edit Distance, Shortest Common Supersequence, Minimum ASCII Delete Sum.

Test cases

# Quick smoke tests, paste into a REPL or save as test_1143.py and run.
# Uses the canonical implementation (Approach 2: 2-D bottom-up DP).
def longest_common_subsequence(text1, text2):
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]:
dp[i][j] = 1 + dp[i - 1][j - 1]
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
def _run_tests():
# problem statement examples
assert longest_common_subsequence("abcde", "ace") == 3
assert longest_common_subsequence("abc", "abc") == 3
assert longest_common_subsequence("abc", "def") == 0
# edge: empty strings
assert longest_common_subsequence("", "abc") == 0
assert longest_common_subsequence("abc", "") == 0
# single char match
assert longest_common_subsequence("a", "a") == 1
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Dynamic Programming, state-and-transition tactics for solving overlapping subproblems with cached answers.
  • Sequence DP, order-aware DP tactics for strings and arrays where prefixes or positions define reusable states.
  • Tabulation, bottom-up DP tactics for filling states in dependency order without recursion.