Longest Common Subsequence in DSA Python - Time & Space Complexity
We want to understand how the time needed to find the longest common subsequence grows as the input strings get longer.
How does the work increase when the strings become bigger?
Analyze the time complexity of the following code snippet.
def lcs(X, Y):
m, n = len(X), len(Y)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if X[i - 1] == Y[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]
This code finds the length of the longest common subsequence between two strings using a table to store results.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Nested loops over both strings to fill the dp table.
- How many times: The inner loop runs for each character of the second string, and the outer loop runs for each character of the first string, so total operations are roughly m x n.
As the length of the strings increases, the number of operations grows by multiplying their lengths.
| Input Size (m, n) | Approx. Operations |
|---|---|
| 10, 10 | 100 |
| 100, 100 | 10,000 |
| 1000, 1000 | 1,000,000 |
Pattern observation: Doubling the length of both strings roughly quadruples the work.
Time Complexity: O(m * n)
This means the time needed grows proportionally to the product of the lengths of the two strings.
[X] Wrong: "The algorithm runs in linear time because it just loops through the strings once."
[OK] Correct: The algorithm uses nested loops, so it checks pairs of characters, making the work multiply, not just add.
Understanding this time complexity helps you explain how dynamic programming improves efficiency and why nested loops matter in real problems.
"What if we used recursion without memoization? How would the time complexity change?"
Practice
Solution
Step 1: Understand the definition of LCS
The LCS is a sequence that appears in both strings in the same order but not necessarily consecutively.Step 2: Differentiate from substring
Unlike substring, LCS does not require characters to be contiguous.Final Answer:
The longest sequence of characters that appear in both strings in the same order, but not necessarily consecutively. -> Option AQuick Check:
LCS = longest ordered sequence [OK]
- Confusing LCS with longest common substring
- Thinking LCS characters must be consecutive
- Mixing LCS with total string length
Solution
Step 1: Understand 2D list initialization
We need a list of lists with (m+1) rows and (n+1) columns filled with zeros.Step 2: Check each option
dp = [[0] * (n+1) for _ in range(m+1)] uses list comprehension correctly to create independent rows. dp = [[0] * (m+1)] * (n+1) uses multiplication which creates references to the same list, causing bugs.Final Answer:
dp = [[0] * (n+1) for _ in range(m+1)] -> Option DQuick Check:
Use list comprehension for 2D lists [OK]
- Using multiplication for 2D lists causing shared rows
- Mixing up m and n dimensions
- Initializing with 1D list instead of 2D
def lcs_length(X, Y):
m, n = len(X), len(Y)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if X[i-1] == Y[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('AGGTAB', 'GXTXAYB'))Solution
Step 1: Understand the LCS calculation
The function calculates the length of the longest common subsequence between 'AGGTAB' and 'GXTXAYB'.Step 2: Trace the LCS
A common LCS is 'GTAB' (G at pos 1 or 2 in X, T at 3, A at 4, B at 5; matching positions in Y: G0, T2, A4, B6), length 4. The standard DP code correctly computes dp[6][7] = 4.Final Answer:
4 -> Option BQuick Check:
LCS length = 4 [OK]
- Counting longest common substring instead of subsequence
- Miscounting characters due to off-by-one errors
- Confusing length with actual subsequence
def lcs_bug(X, Y):
m, n = len(X), len(Y)
dp = [[0] * (m+1) for _ in range(n+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if X[i-1] == Y[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_bug('ABC', 'AC'))Solution
Step 1: Check dp array initialization
The dp array is initialized with dimensions (n+1) x (m+1), which is swapped. It should be (m+1) x (n+1) to match loops.Step 2: Consequences of swapped dimensions
This causes index errors or incorrect results because dp[i][j] expects i in [0..m], j in [0..n].Final Answer:
The dp array dimensions are swapped; should be (m+1) x (n+1). -> Option AQuick Check:
dp size matches string lengths [OK]
- Swapping m and n in dp initialization
- Starting loops at 0 instead of 1
- Using wrong indices for string characters
X = 'ABCBDAB' and Y = 'BDCABA', which of the following is a valid Longest Common Subsequence?Solution
Step 1: Identify LCS length and candidates
The LCS length for these strings is 4. Verify which option of length 4 appears in order in both X='ABCBDAB' and Y='BDCABA'.Step 2: Verify each option
A: 'BDAB' - In X: B(1), D(4), A(5), B(6); in Y: B(0), D(1), A(3), B(4). Order preserved.
B: 'BCBD' - Requires B then C(2) then B(4) then D after 4, but no D after pos 4 in Y (only D at 1).
C: 'BBCA' - Requires two B's before C(2) in Y, but only one B at 0 before C(2).
D: 'BDA' length 3 < 4.Final Answer:
'BDAB' -> Option CQuick Check:
Check subsequence order in both strings [OK]
- Choosing subsequences not in order in both strings
- Picking shorter subsequences ignoring max length
- Confusing subsequence with substring
Ready to prove it?
Pass the test and this pattern moves unknown
