Longest Common Subsequence in DSA Rust - 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.
fn lcs(s1: &str, s2: &str) -> usize {
let m = s1.len();
let n = s2.len();
let mut dp = vec![vec![0; n + 1]; m + 1];
for i in 1..=m {
for j in 1..=n {
if s1.as_bytes()[i - 1] == s2.as_bytes()[j - 1] {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
}
}
}
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: Outer loop runs m times, inner loop runs n times, so total m x n times.
As the strings get longer, the work 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 roughly squares the work needed.
Time Complexity: O(m × n)
This means the time needed grows proportionally to the product of the two string lengths.
[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 compares every character of one string with every character of the other, making the work multiply, not just add.
Understanding this time complexity helps you explain how dynamic programming improves efficiency by avoiding repeated work, a key skill in problem solving.
"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 of characters that appear in both strings in the same order, but not necessarily consecutively.Step 2: Differentiate from substring and other concepts
Unlike substring, LCS does not require characters to be consecutive. It is about order and presence.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 common sequence [OK]
- Confusing LCS with longest common substring
- Thinking characters must be consecutive
- Mixing LCS with edit distance
Solution
Step 1: Identify correct parameter types for string inputs
Using &str is idiomatic for string slices in Rust, allowing borrowing without ownership transfer.Step 2: Identify correct return type for LCS length
LCS length is a number, so usize is appropriate. Returning &str or String is incorrect for length.Final Answer:
fn lcs_length(s1: &str, s2: &str) -> usize -> Option DQuick Check:
Input: &str, Output: usize for length [OK]
- Returning &str or String instead of length
- Using owned String unnecessarily
- Returning bool instead of length
fn main() {
let s1 = "AGGTAB";
let s2 = "GXTXAYB";
let lcs_len = lcs_length(s1, s2);
println!("{}", lcs_len);
}
fn lcs_length(s1: &str, s2: &str) -> usize {
let m = s1.len();
let n = s2.len();
let mut dp = vec![vec![0; n + 1]; m + 1];
for i in 1..=m {
for j in 1..=n {
if s1.as_bytes()[i - 1] == s2.as_bytes()[j - 1] {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
}
}
}
dp[m][n]
}Solution
Step 1: Understand the input strings and LCS algorithm
Strings are "AGGTAB" and "GXTXAYB". The code uses dynamic programming to find LCS length.Step 2: Trace the LCS calculation
The LCS is "GTAB" with length 4. The code correctly computes this.Step 3: Verify carefully
Common subsequence: G T A B (positions match in order). Length is 4.Final Answer:
4 -> Option CQuick Check:
LCS length of "AGGTAB" and "GXTXAYB" = 4 [OK]
- Counting common characters not in order
- Confusing substring with subsequence
- Off-by-one errors in indexing
fn lcs_length(s1: &str, s2: &str) -> usize {
let m = s1.len();
let n = s2.len();
let mut dp = vec![vec![0; n]; m];
for i in 1..m {
for j in 1..n {
if s1.as_bytes()[i] == s2.as_bytes()[j] {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
}
}
}
dp[m - 1][n - 1]
}Solution
Step 1: Check dp vector initialization
The dp vector is initialized as m x n, but it should be (m+1) x (n+1) to handle base cases and indexing from 1.Step 2: Understand loop ranges and indexing
Loops start from 1..Final Answer:
The dp vector is initialized with size m x n instead of (m+1) x (n+1). -> Option AQuick Check:
dp size must be (m+1) x (n+1) [OK]
- Initializing dp with wrong size
- Incorrect loop ranges causing panic
- Wrong indexing of string bytes
Solution
Step 1: Understand dp table usage for reconstruction
The dp table stores lengths of LCS for substrings. To reconstruct, start from dp[m][n] and move backwards.Step 2: Trace path to build LCS string
If characters match at s1[i-1] and s2[j-1], move diagonally up-left and add character. Else move to cell with larger dp value (up or left).Step 3: Reverse collected characters
Since traversal is from end to start, collected characters must be reversed to get correct LCS.Final Answer:
Start from dp[m][n], move diagonally up-left if characters match, else move to the cell with the larger value, collecting matched characters in reverse order. -> Option BQuick Check:
Backtrack dp table from bottom-right to reconstruct LCS [OK]
- Starting reconstruction from top-left
- Not reversing collected characters
- Ignoring dp values when moving
Ready to prove it?
Pass the test and this pattern moves unknown
