Longest Common Subsequence in DSA Go - 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.
func LCS(s1, s2 string) int {
m, n := len(s1), len(s2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
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]
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
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: Outer loop runs m times, inner loop runs n times, so total m x n times.
As the strings get longer, the number of operations grows by multiplying their lengths.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 100 |
| 100 | 10,000 |
| 1000 | 1,000,000 |
Pattern observation: Doubling the length roughly squares the work needed.
Time Complexity: O(m x 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 over simple recursion and why this approach is efficient for comparing sequences.
"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 the longest sequence that appears in both strings in the same order, but characters do not need to be next to each other.Step 2: Differentiate from substring
A substring requires characters to be consecutive, but LCS allows gaps.Final Answer:
The longest sequence of characters that appear in both strings in the same order, but not necessarily consecutively. -> Option BQuick Check:
LCS = longest ordered sequence [OK]
- Confusing LCS with longest substring
- Thinking LCS characters must be consecutive
- Mixing LCS with string concatenation
s1 and s2?Solution
Step 1: Identify input types for LCS function
The LCS function takes two strings as input, so parameters should be of type string.Step 2: Identify output type for LCS function
The function returns the length of the LCS, which is an integer.Final Answer:
func lcs(s1, s2 string) int -> Option AQuick Check:
Input strings, output int length [OK]
- Using wrong input types like int or slices
- Returning string instead of length
- Mixing input and output types
package main
import "fmt"
func lcs(s1, s2 string) int {
m, n := len(s1), len(s2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if s1[i-1] == s2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
if dp[i-1][j] > dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
}
return dp[m][n]
}
func main() {
fmt.Println(lcs("AGGTAB", "GXTXAYB"))
}Solution
Step 1: Understand the input strings
Strings are "AGGTAB" and "GXTXAYB". We find their LCS length using dynamic programming.Step 2: Trace the LCS calculation
The LCS is "GTAB" which has length 4, but checking carefully the code and strings, the LCS is actually "GTAB" (length 4) or "GTTAB"? The correct LCS is "GTAB" length 4 or "GTAB" length 4? Actually, the LCS is "GTAB" length 4. But the code returns 4 or 5? Let's check carefully: The LCS is "GTAB" (G T A B) length 4. But the code returns dp[m][n], which is 4. But options have 4 and 5. Let's verify carefully: The LCS is "GTAB" length 4. So output is 4.Final Answer:
4 -> Option AQuick Check:
LCS length of "AGGTAB" and "GXTXAYB" = 4 [OK]
- Confusing LCS length with longest substring length
- Off-by-one errors in indexing
- Misreading input strings
func lcs(s1, s2 string) int {
m, n := len(s1), len(s2)
dp := make([][]int, m)
for i := range dp {
dp[i] = make([]int, n)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if s1[i-1] == s2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
if dp[i-1][j] > dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
}
return dp[m-1][n-1]
}Solution
Step 1: Check dp slice initialization
The dp slice is created with size m and n, but the loops access dp[i][j] for i and j up to m and n inclusive, causing index out of range errors.Step 2: Correct dp size for safe indexing
dp should be initialized with size m+1 and n+1 to safely access dp[i][j] where i and j go up to m and n.Final Answer:
The dp slice is not initialized with size m+1 and n+1, causing index out of range errors. -> Option CQuick Check:
dp size must be m+1 x n+1 [OK]
- Initializing dp with size m and n instead of m+1 and n+1
- Starting loops at 1 but dp size too small
- Incorrect return index
s1 = "ABCBDAB" and s2 = "BDCABA", write a Go function that returns the actual Longest Common Subsequence string, not just its length. Which approach correctly reconstructs the LCS from the DP table?Solution
Step 1: Build DP table for LCS length
Use dynamic programming to fill dp table with lengths of LCS for substrings of s1 and s2.Step 2: Backtrack to reconstruct LCS string
Start from dp[m][n], move diagonally up-left if characters match (add character to result), else move in direction of larger dp value until reaching dp[0][0].Final Answer:
Build the DP table for LCS length, then backtrack from dp[m][n] moving diagonally when characters match, else move in direction of larger dp value. -> Option DQuick Check:
DP + backtracking reconstructs LCS string [OK]
- Returning dp[m][n] as string instead of length
- Not backtracking to reconstruct sequence
- Sorting strings loses original order
Ready to prove it?
Pass the test and this pattern moves unknown
