ExamplesBy LevelBy TopicLearning Paths
786 Intermediate

786-longest-common-subsequence — Longest Common Subsequence

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "786-longest-common-subsequence — Longest Common Subsequence" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The Longest Common Subsequence (LCS) problem finds the longest sequence of characters common to two strings (not necessarily contiguous). Key difference from OCaml: 1. **Array access**: Rust's `Vec<Vec<usize>>` indexing with `dp[i][j]`; OCaml's `dp.(i).(j)` — syntactically different but equivalent.

Tutorial

The Problem

The Longest Common Subsequence (LCS) problem finds the longest sequence of characters common to two strings (not necessarily contiguous). It is the foundation of diff tools (used in Git, patch, and code review), DNA sequence alignment (BLAST, ClustalW), and plagiarism detection. The classic O(mn) DP solution by Hirschberg (1975) fills an m×n table comparing characters.

🎯 Learning Outcomes

  • • Fill a 2D DP table dp[i][j] = LCS length of a[:i] and b[:j]
  • • Apply the recurrence: match increases by 1, mismatch takes max of adjacent cells
  • • Reconstruct the actual LCS string by backtracking through the table
  • • Understand the relationship between LCS and edit distance (Levenshtein)
  • • See how LCS extends to multiple sequences (3-way LCS) at higher complexity
  • Code Example

    #![allow(clippy::all)]
    //! # Longest Common Subsequence
    
    pub fn lcs(a: &str, b: &str) -> usize {
        let (m, n) = (a.len(), b.len());
        let a: Vec<char> = a.chars().collect();
        let b: Vec<char> = b.chars().collect();
        let mut dp = vec![vec![0; n + 1]; m + 1];
    
        for i in 1..=m {
            for j in 1..=n {
                dp[i][j] = if a[i - 1] == b[j - 1] {
                    dp[i - 1][j - 1] + 1
                } else {
                    dp[i - 1][j].max(dp[i][j - 1])
                };
            }
        }
        dp[m][n]
    }
    
    pub fn lcs_string(a: &str, b: &str) -> String {
        let (m, n) = (a.len(), b.len());
        let a: Vec<char> = a.chars().collect();
        let b: Vec<char> = b.chars().collect();
        let mut dp = vec![vec![0; n + 1]; m + 1];
    
        for i in 1..=m {
            for j in 1..=n {
                dp[i][j] = if a[i - 1] == b[j - 1] {
                    dp[i - 1][j - 1] + 1
                } else {
                    dp[i - 1][j].max(dp[i][j - 1])
                };
            }
        }
    
        let mut result = Vec::new();
        let (mut i, mut j) = (m, n);
        while i > 0 && j > 0 {
            if a[i - 1] == b[j - 1] {
                result.push(a[i - 1]);
                i -= 1;
                j -= 1;
            } else if dp[i - 1][j] > dp[i][j - 1] {
                i -= 1;
            } else {
                j -= 1;
            }
        }
        result.iter().rev().collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_lcs() {
            assert_eq!(lcs("ABCDGH", "AEDFHR"), 3);
        }
        #[test]
        fn test_lcs_string() {
            assert_eq!(lcs_string("ABCDGH", "AEDFHR"), "ADH");
        }
        #[test]
        fn test_empty() {
            assert_eq!(lcs("", "ABC"), 0);
        }
        #[test]
        fn test_identical() {
            assert_eq!(lcs("ABC", "ABC"), 3);
        }
    }

    Key Differences

  • Array access: Rust's Vec<Vec<usize>> indexing with dp[i][j]; OCaml's dp.(i).(j) — syntactically different but equivalent.
  • String iteration: Rust collects chars() into a Vec<char> for indexed access; OCaml uses Bytes.get on a Bytes.of_string copy.
  • Backtracking: Both use iterative backtracking with while loops — identical structure.
  • Space optimization: Hirschberg's O(n) space LCS algorithm is expressible in both languages; neither standard library includes it.
  • OCaml Approach

    OCaml implements LCS with the same 2D array approach. let dp = Array.make_matrix (m+1) (n+1) 0 in .... Backtracking uses a recursive function that pattern-matches on the dp values. OCaml's List.rev is often used to accumulate characters in reverse during backtracking. The Diff library wraps LCS for line-level diff computation similar to git diff.

    Full Source

    #![allow(clippy::all)]
    //! # Longest Common Subsequence
    
    pub fn lcs(a: &str, b: &str) -> usize {
        let (m, n) = (a.len(), b.len());
        let a: Vec<char> = a.chars().collect();
        let b: Vec<char> = b.chars().collect();
        let mut dp = vec![vec![0; n + 1]; m + 1];
    
        for i in 1..=m {
            for j in 1..=n {
                dp[i][j] = if a[i - 1] == b[j - 1] {
                    dp[i - 1][j - 1] + 1
                } else {
                    dp[i - 1][j].max(dp[i][j - 1])
                };
            }
        }
        dp[m][n]
    }
    
    pub fn lcs_string(a: &str, b: &str) -> String {
        let (m, n) = (a.len(), b.len());
        let a: Vec<char> = a.chars().collect();
        let b: Vec<char> = b.chars().collect();
        let mut dp = vec![vec![0; n + 1]; m + 1];
    
        for i in 1..=m {
            for j in 1..=n {
                dp[i][j] = if a[i - 1] == b[j - 1] {
                    dp[i - 1][j - 1] + 1
                } else {
                    dp[i - 1][j].max(dp[i][j - 1])
                };
            }
        }
    
        let mut result = Vec::new();
        let (mut i, mut j) = (m, n);
        while i > 0 && j > 0 {
            if a[i - 1] == b[j - 1] {
                result.push(a[i - 1]);
                i -= 1;
                j -= 1;
            } else if dp[i - 1][j] > dp[i][j - 1] {
                i -= 1;
            } else {
                j -= 1;
            }
        }
        result.iter().rev().collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_lcs() {
            assert_eq!(lcs("ABCDGH", "AEDFHR"), 3);
        }
        #[test]
        fn test_lcs_string() {
            assert_eq!(lcs_string("ABCDGH", "AEDFHR"), "ADH");
        }
        #[test]
        fn test_empty() {
            assert_eq!(lcs("", "ABC"), 0);
        }
        #[test]
        fn test_identical() {
            assert_eq!(lcs("ABC", "ABC"), 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_lcs() {
            assert_eq!(lcs("ABCDGH", "AEDFHR"), 3);
        }
        #[test]
        fn test_lcs_string() {
            assert_eq!(lcs_string("ABCDGH", "AEDFHR"), "ADH");
        }
        #[test]
        fn test_empty() {
            assert_eq!(lcs("", "ABC"), 0);
        }
        #[test]
        fn test_identical() {
            assert_eq!(lcs("ABC", "ABC"), 3);
        }
    }

    Deep Comparison

    OCaml vs Rust: Longest Common Subsequence

    Overview

    See the example.rs and example.ml files for detailed implementations.

    Key Differences

    AspectOCamlRust
    Type systemHindley-MilnerOwnership + traits
    MemoryGCZero-cost abstractions
    MutabilityExplicit refmut keyword
    Error handlingOption/ResultResult<T, E>

    See README.md for detailed comparison.

    Exercises

  • Implement lcs_diff(a: &[&str], b: &[&str]) -> Vec<DiffOp> that returns a diff as a list of Keep, Insert, and Delete operations — the basis for git diff.
  • Implement the space-optimized O(m+n) space LCS using Hirschberg's divide-and-conquer algorithm.
  • Extend to three sequences: lcs3(a, b, c) using a 3D DP table, and verify it on biological sequences.
  • Open Source Repos