Master Longest Common Subsequence (LCS) completely — definition with clear examples, 2D DP table construction, string reconstruction, space optimization, and all important variants including Longest Common Substring, Edit Distance, Shortest Common Supersequence, and real-world applications.
Definition
A subsequence is a sequence derived from another sequence by deleting some (or no) elements without changing the order of the remaining elements.
| String | "ABCBDAB" |
| Valid subsequences | "A", "ABC", "BDB", "ABCB", "ABAB", "ABCBDAB", "" |
| NOT a subsequence | "BA" would require reordering |
The Longest Common Subsequence (LCS) of two strings is the longest sequence that is a subsequence of both.
s1 = "ABCBDAB"
s2 = "BDCAB"
Common subsequences: "B", "BC", "BA", "BDA", "BCAB", "BDAB", "BCAB"
LCS: "BCAB" or "BDAB" — both have length 4
Complete Implementation
def lcs_length(s1, s2):
"""
Find length of Longest Common Subsequence.
Time: O(m×n), Space: O(m×n)
"""
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
print(lcs_length("ABCBDAB", "BDCAB")) # 4
print(lcs_length("AGGTAB", "GXTXAYB")) # 4 (GTAB)
DP Table Visualization
For s1="ABCD", s2="ACBD":
| dp[4][4] = 3 | LCS length is 3 |
| dp[4][4]=3: s1[3]='D' ≠ s2[3]='D' | wait, 'D'='D'! → came from dp[3][3]+1=2+1=3 |
| dp[3][3]=2: s1[2]='C' vs s2[2]='B' | not equal → came from dp[2][3]=2 (max of dp[2][3],dp[3][2]) |
| dp[2][3]=2: s1[1]='B' vs s2[2]='B' | EQUAL → came from dp[1][2]+1=1+1=2 |
| dp[1][2]=1: s1[0]='A' vs s2[1]='C' | not equal → came from dp[0][2]=0 or dp[1][1]=1 → dp[1][1] |
| dp[1][1]=1: s1[0]='A' vs s2[0]='A' | EQUAL → came from dp[0][0]+1=0+1=1 |
Reconstruct the Actual LCS String
def lcs_reconstruct(s1, s2):
"""
Find and return the actual LCS string.
Time: O(m×n), Space: O(m×n)
"""
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# Backtrack to find the string
result = []
i, j = m, n
while i > 0 and j > 0:
if s1[i-1] == s2[j-1]:
result.append(s1[i-1])
i -= 1; j -= 1
elif dp[i-1][j] > dp[i][j-1]:
i -= 1
else:
j -= 1
return ''.join(reversed(result))
print(lcs_reconstruct("ABCBDAB", "BDCAB")) # "BCAB" or "BDAB"
Space Optimization — O(n) Space
def lcs_space_optimized(s1, s2):
"""
Only keep two rows: previous and current.
Time: O(m×n), Space: O(n)
"""
if len(s1) < len(s2):
s1, s2 = s2, s1 # Make s1 the longer string
m, n = len(s1), len(s2)
prev = [0] * (n + 1)
for i in range(1, m+1):
curr = [0] * (n+1)
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev = curr
return prev[n]
print(lcs_space_optimized("ABCBDAB", "BDCAB")) # 4
Important LCS Variants
Variant 1 — Longest Common Substring (Contiguous)
Unlike subsequence, substring must be contiguous:
def longest_common_substring(s1, s2):
"""
Longest contiguous substring common to both strings.
dp[i][j] = length of longest common suffix of s1[:i] and s2[:j]
Time: O(m×n), Space: O(m×n) reducible to O(n)
"""
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
max_length = 0
end_pos = 0 # End index in s1 of longest common substring
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # Extend from previous
if dp[i][j] > max_length:
max_length = dp[i][j]
end_pos = i
else:
dp[i][j] = 0 # Reset — must be contiguous
return s1[end_pos-max_length:end_pos], max_length
print(longest_common_substring("ABAB", "BABA")) # ('BAB', 3) or ('ABA', 3)
print(longest_common_substring("GeeksforGeeks", "GeeksQuiz")) # ('Geeks', 5)
Key difference: When characters don't match, LCS variant takes max(left, up), but substring variant resets to 0.
Variant 2 — Edit Distance (Levenshtein)
Minimum operations (insert, delete, substitute) to transform s1 into s2:
def edit_distance(s1, s2):
"""
dp[i][j] = min edits to transform s1[:i] into s2[:j]
Recurrence:
If s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] (no edit needed)
Else: dp[i][j] = 1 + min(
dp[i-1][j], # Delete from s1
dp[i][j-1], # Insert into s1
dp[i-1][j-1] # Substitute
)
"""
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = i # Delete all i chars
for j in range(n+1): dp[0][j] = j # Insert all j chars
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
return dp[m][n]
print(edit_distance("kitten", "sitting")) # 3
print(edit_distance("sunday", "saturday")) # 3
print(edit_distance("", "abc")) # 3
Variant 3 — Shortest Common Supersequence
Find the shortest string that has both s1 and s2 as subsequences:
def shortest_common_supersequence(s1, s2):
"""
SCS length = len(s1) + len(s2) - LCS(s1, s2)
Because: include all of both, but LCS is counted only once.
"""
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
if s1[i-1]==s2[j-1]: dp[i][j]=dp[i-1][j-1]+1
else: dp[i][j]=max(dp[i-1][j],dp[i][j-1])
lcs_len = dp[m][n]
scs_len = m + n - lcs_len
# Reconstruct the actual SCS
result = []
i, j = m, n
while i > 0 and j > 0:
if s1[i-1] == s2[j-1]:
result.append(s1[i-1])
i -= 1; j -= 1
elif dp[i-1][j] > dp[i][j-1]:
result.append(s1[i-1]); i -= 1
else:
result.append(s2[j-1]); j -= 1
while i > 0: result.append(s1[i-1]); i -= 1
while j > 0: result.append(s2[j-1]); j -= 1
return ''.join(reversed(result)), scs_len
scs, length = shortest_common_supersequence("AGGTAB", "GXTXAYB")
print(scs, length) # Length = 9
Variant 4 — Count LCS (Number of LCS of Maximum Length)
def count_lcs(s1, s2):
"""Count how many distinct LCS strings exist."""
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
count = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): count[i][0] = 1
for j in range(n+1): count[0][j] = 1
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
count[i][j] = count[i-1][j-1]
elif dp[i-1][j] > dp[i][j-1]:
dp[i][j] = dp[i-1][j]
count[i][j] = count[i-1][j]
elif dp[i-1][j] < dp[i][j-1]:
dp[i][j] = dp[i][j-1]
count[i][j] = count[i][j-1]
else:
dp[i][j] = dp[i-1][j]
count[i][j] = count[i-1][j] + count[i][j-1]
# Subtract overlap if dp[i-1][j-1] == dp[i][j]
if dp[i-1][j-1] == dp[i][j]:
count[i][j] -= count[i-1][j-1]
return dp[m][n], count[m][n]
Real-World Applications of LCS
| 1. Git diff | Shows lines added/removed between two versions of a file |
| 2. DNA Sequence Alignment | Compare two DNA sequences to find shared genes |
| 3. Spell Checkers | Compute edit distance between typed word and dictionary |
| 4. Plagiarism Detection | Find longest common sections in two documents |
| 5. File Comparison Tools | diff, merge, patch utilities |
Complexity Summary
| Problem | Time | Space |
|---|
| LCS Length | O(m×n) | O(m×n) |
| LCS with reconstruction | O(m×n) | O(m×n) |
| LCS space-optimized | O(m×n) | O(n) |
| Longest Common Substring | O(m×n) | O(m×n) |
| Edit Distance | O(m×n) | O(m×n) |
| Shortest Common Supersequence | O(m×n) | O(m×n) |
Summary
LCS is the foundation of all string DP problems:
- LCS:
dp[i][j] = dp[i-1][j-1]+1 if match, else max(left, up) - Substring:
dp[i][j] = dp[i-1][j-1]+1 if match, else 0 (no max — reset!) - Edit Distance: 3 choices: delete, insert, substitute — take min
- SCS: length = m + n - LCS (combine but share the LCS)
The 2D DP table structure is identical for all these — only the recurrence changes.
*Next Lesson: Longest Increasing Subsequence*