Bird
Raised Fist0
DSA Rustprogramming

Longest Common Subsequence in DSA Rust

Choose your learning style10 modes available
Mental Model
Find the longest sequence of characters that appear in the same order in both strings, but not necessarily consecutively.
Analogy: Imagine two friends telling stories with some common events. The longest common subsequence is like the longest list of shared events they both mention in the same order, even if they skip some details.
String A: A B C D G H
String B: A E D F H R

LCS path:
A ->   -> D ->   -> H

Where arrows show matching characters in order, skipping unmatched ones.
Dry Run Walkthrough
Input: String A: "ABCDGH", String B: "AEDFHR"
Goal: Find the longest common subsequence between the two strings.
Step 1: Initialize a 2D table with zeros for lengths of subsequences.
Table rows: 7 (for A length +1), columns: 7 (for B length +1), all zeros.
Why: We need a table to store lengths of LCS for substrings to build up the answer.
Step 2: Compare first characters: 'A' and 'A' match, set table[1][1] = 1.
Table[1][1] = 1, rest zeros.
Why: Matching characters increase LCS length by 1.
Step 3: Compare 'B' (A[1]) and 'E' (B[1]), no match, take max from left or top: table[2][2] = 1.
Table[2][2] = 1, previous matches carried forward.
Why: No match means LCS length stays as max of previous substrings.
Step 4: Compare 'D' (A[3]) and 'D' (B[2]), match found, table[4][3] = table[3][2] + 1 = 2.
Table[4][3] = 2, showing LCS length increased.
Why: Matching characters extend the LCS by 1 from previous substrings.
Step 5: Continue filling table comparing all characters, updating with max or increment on match.
Final table shows max LCS length 3 at table[6][6].
Why: Complete table helps find the longest subsequence length.
Step 6: Trace back from table[6][6] to find actual subsequence: 'A', 'D', 'H'.
LCS sequence: A -> D -> H
Why: Backtracking finds the characters that form the longest subsequence.
Result:
LCS length = 3
LCS sequence = A -> D -> H
Annotated Code
DSA Rust
fn longest_common_subsequence(a: &str, b: &str) -> (usize, String) {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let m = a_chars.len();
    let n = b_chars.len();
    let mut dp = vec![vec![0; n + 1]; m + 1];

    for i in 1..=m {
        for j in 1..=n {
            if a_chars[i - 1] == b_chars[j - 1] {
                dp[i][j] = dp[i - 1][j - 1] + 1; // match found, extend LCS
            } else {
                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]); // no match, take max
            }
        }
    }

    // Backtrack to find the LCS string
    let mut lcs = Vec::new();
    let (mut i, mut j) = (m, n);
    while i > 0 && j > 0 {
        if a_chars[i - 1] == b_chars[j - 1] {
            lcs.push(a_chars[i - 1]); // add matching char
            i -= 1; j -= 1; // move diagonally up-left
        } else if dp[i - 1][j] > dp[i][j - 1] {
            i -= 1; // move up
        } else {
            j -= 1; // move left
        }
    }
    lcs.reverse();
    (dp[m][n], lcs.into_iter().collect())
}

fn main() {
    let a = "ABCDGH";
    let b = "AEDFHR";
    let (length, sequence) = longest_common_subsequence(a, b);
    println!("LCS length = {}", length);
    println!("LCS sequence = {}", sequence);
}
dp[i][j] = dp[i - 1][j - 1] + 1; // match found, extend LCS
extend LCS length by 1 when characters match
dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]); // no match, take max
choose max LCS length from previous substrings when no match
while i > 0 && j > 0 { ... }
backtrack from bottom-right to find LCS characters
lcs.push(a_chars[i - 1]); // add matching char
record matching character in LCS
lcs.reverse();
reverse collected characters to correct order
OutputSuccess
LCS length = 3 LCS sequence = ADH
Complexity Analysis
Time: O(m*n) because we fill a table of size m by n once
Space: O(m*n) because we store LCS lengths for all substring pairs
vs Alternative: Naive recursive approach is exponential due to repeated subproblems; DP reduces it to polynomial time
Edge Cases
One or both strings empty
LCS length is zero and sequence is empty
DSA Rust
let m = a_chars.len(); let n = b_chars.len();
Strings with no common characters
LCS length is zero and sequence is empty
DSA Rust
dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
Strings are identical
LCS is the entire string itself
DSA Rust
dp[i][j] = dp[i - 1][j - 1] + 1;
When to Use This Pattern
When you need to find the longest sequence common to two strings in order, use the Longest Common Subsequence pattern because it efficiently finds shared subsequences using dynamic programming.
Common Mistakes
Mistake: Not initializing the DP table with extra row and column for empty substrings
Fix: Initialize dp with size (m+1) x (n+1) to handle empty substring cases
Mistake: Incorrect backtracking direction causing wrong LCS sequence
Fix: Backtrack by moving diagonally on match, else move in direction of larger dp value
Summary
Finds the longest sequence of characters common to two strings in order.
Use when comparing sequences to find shared patterns or similarities.
The key is building a table of LCS lengths and backtracking to get the sequence.

Practice

(1/5)
1. What does the Longest Common Subsequence (LCS) between two strings represent?
easy
A. The longest sequence of characters that appear in both strings in the same order, but not necessarily consecutively.
B. The longest substring that appears exactly the same in both strings.
C. The shortest sequence of characters that can be removed to make the strings identical.
D. The total number of characters that differ between the two strings.

Solution

  1. 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.
  2. Step 2: Differentiate from substring and other concepts

    Unlike substring, LCS does not require characters to be consecutive. It is about order and presence.
  3. Final Answer:

    The longest sequence of characters that appear in both strings in the same order, but not necessarily consecutively. -> Option A
  4. Quick Check:

    LCS = longest ordered common sequence [OK]
Hint: LCS keeps order, skips gaps, unlike substring [OK]
Common Mistakes:
  • Confusing LCS with longest common substring
  • Thinking characters must be consecutive
  • Mixing LCS with edit distance
2. Which of the following Rust function signatures correctly represents a function to compute LCS length between two string slices?
easy
A. fn lcs_length(s1: &str, s2: &str) -> bool
B. fn lcs_length(s1: String, s2: String) -> &str
C. fn lcs_length(s1: &String, s2: &String) -> String
D. fn lcs_length(s1: &str, s2: &str) -> usize

Solution

  1. Step 1: Identify correct parameter types for string inputs

    Using &str is idiomatic for string slices in Rust, allowing borrowing without ownership transfer.
  2. 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.
  3. Final Answer:

    fn lcs_length(s1: &str, s2: &str) -> usize -> Option D
  4. Quick Check:

    Input: &str, Output: usize for length [OK]
Hint: Use &str for inputs and usize for length output [OK]
Common Mistakes:
  • Returning &str or String instead of length
  • Using owned String unnecessarily
  • Returning bool instead of length
3. What is the output of the following Rust code snippet that computes the LCS length of "AGGTAB" and "GXTXAYB"?
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]
}
medium
A. 6
B. 3
C. 4
D. 5

Solution

  1. Step 1: Understand the input strings and LCS algorithm

    Strings are "AGGTAB" and "GXTXAYB". The code uses dynamic programming to find LCS length.
  2. Step 2: Trace the LCS calculation

    The LCS is "GTAB" with length 4. The code correctly computes this.
  3. Step 3: Verify carefully

    Common subsequence: G T A B (positions match in order). Length is 4.
  4. Final Answer:

    4 -> Option C
  5. Quick Check:

    LCS length of "AGGTAB" and "GXTXAYB" = 4 [OK]
Hint: Trace matching chars in order, ignore gaps [OK]
Common Mistakes:
  • Counting common characters not in order
  • Confusing substring with subsequence
  • Off-by-one errors in indexing
4. Identify the bug in the following Rust function that attempts to compute the LCS length:
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]
}
medium
A. The dp vector is initialized with size m x n instead of (m+1) x (n+1).
B. The loops start from 0 instead of 1, causing index errors.
C. The loops use 1..=m and 1..=n instead of 1..m and 1..n, causing out of bounds.
D. The function returns dp[m][n] which is out of bounds.

Solution

  1. 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.
  2. Step 2: Understand loop ranges and indexing

    Loops start from 1..
  3. Final Answer:

    The dp vector is initialized with size m x n instead of (m+1) x (n+1). -> Option A
  4. Quick Check:

    dp size must be (m+1) x (n+1) [OK]
Hint: dp table size must include extra row and column [OK]
Common Mistakes:
  • Initializing dp with wrong size
  • Incorrect loop ranges causing panic
  • Wrong indexing of string bytes
5. Given two strings, write a Rust function that returns the actual Longest Common Subsequence string (not just length). Which approach below correctly reconstructs the LCS from the dp table?
hard
A. Start from dp[0][0], move right or down collecting characters whenever dp[i][j] increases.
B. 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.
C. Traverse dp table row-wise and append characters when dp[i][j] is non-zero.
D. Reverse both strings and find LCS length, then reverse the result to get LCS string.

Solution

  1. 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.
  2. 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).
  3. Step 3: Reverse collected characters

    Since traversal is from end to start, collected characters must be reversed to get correct LCS.
  4. 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 B
  5. Quick Check:

    Backtrack dp table from bottom-right to reconstruct LCS [OK]
Hint: Backtrack dp from bottom-right, collect matches, then reverse [OK]
Common Mistakes:
  • Starting reconstruction from top-left
  • Not reversing collected characters
  • Ignoring dp values when moving
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard