DSA DSA In Depth
0%
lesson lcsedit-distancelevenshtein

Sequence Alignment DP: Longest Common Subsequence (LCS) & Wagner-Fischer Edit Distance

7 min read Module 6.2: Dynamic Programming Foundations & Classical Archetypes Updated Oct 23, 2026
📄 Cheat Sheet
Longest Common Subsequence — Dynamic Programming & Competing Subproblems
Back To Back SWE Longest Common Subsequence — Dynamic Programming & Competing Subproblems Watch on YouTube ↗

Erik Demaine derives the Wagner-Fischer Edit Distance DAG, sequence alignment in computational biology, and prefix-pair subproblem spaces.

0:00 / --:--

1. The Alignment Grid DAG: Wagner-Fischer Edit Distance & LCS

Given strings $A[0..n-1]$ and $B[0..m-1]$, prefix-pair Dynamic Programming defines subproblem $(i, j)$ on prefixes $A[0..i-1]$ and $B[0..j-1]$ across an $(n+1) \times (m+1)$ grid DAG with three incoming edges into $(i, j)$:

  • Wagner–Fischer Levenshtein Edit Distance (1974): Minimum insertions, deletions, and substitutions to transform $A$ into $B$: $$dp[i][j] = \begin{cases} dp[i-1][j-1] & \text{if } A[i-1] = B[j-1] \\ 1 + \min\big(\underbrace{dp[i-1][j]}_{\text{Delete } A[i-1]},\; \underbrace{dp[i][j-1]}_{\text{Insert } B[j-1]},\; \underbrace{dp[i-1][j-1]}_{\text{Substitute}}\big) & \text{if } A[i-1] \neq B[j-1] \end{cases}$$ with base cases $dp[i][0] = i$ and $dp[0][j] = j$.
  • Deletion-Insertion Edit Distance vs. LCS Duality: When substitutions are forbidden (or cost $2$), transforming $A$ into $B$ deletes $n - |\text{LCS}|$ characters and inserts $m - |\text{LCS}|$ characters: $$d_{\text{del/ins}}(A, B) = n + m - 2 \cdot |\text{LCS}(A, B)|$$

Core Invariant — Ukkonen's $O(N \cdot K)$ Banded Diagonal Optimization & Hunt-Szymanski Reduction

1. Ukkonen's Banded Edit Distance (1985): Whenever we only care if $\text{EditDistance}(A, B) \le K$ (e.g., fuzzy search or spell-checking with threshold $K \ll N$), any path in the alignment grid that strays further than $K$ diagonals away from the main diagonal ($|i - j| > K$) must pay at least $|i - j| > K$ insertions/deletions! Restricting DP evaluation to the band $j \in [\max(0, i - K), \min(m, i + K)]$ reduces time from $O(N \cdot M)$ to $O(N \cdot K)$.
2. Hunt–Szymanski LCS-to-LIS Reduction: When all elements of $A$ are distinct (e.g., a permutation), mapping each character in $B$ to its index in $A$ transforms LCS into Longest Increasing Subsequence (LIS) in $O((N + M) \log N)$ time! Even with duplicates, mapping each $B[j]$ to its occurrences in $A$ sorted in descending order reduces LCS to LIS in $O(R \log N)$ where $R$ is the number of matching pairs.

DP State DAG, 0/1 Knapsack Reverse Sweep & Myers Diff
O(N·W) / O(N·D) Edit Graph

Observe why 0/1 Knapsack iterates capacity w downwards (preventing item reuse), how Patience Sorting finds LIS in O(N log N), and how Myers Diff slides along diagonal snakes k = x - y.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Item 1 (wt=3, val=4): Sweep w = 7 downTo 3 so dp[w - 3] always reads the previous item layer.
🔗 Live Pointer & State Transition Graph — DP State DAG, 0/1 Knapsack Reverse Sweep & Myers Diff Active Pointer Jump/Transition Matched/Optimal
w=0 0 [0] 0 [1] 0 [2] wt=3, v 4 [3] 4 [4] 4 [5] 4 [6] dp[7]=4 4 [7]
[0]0w=0
[1]0
[2]0
[3]4wt=3, val=4
[4]4
[5]4
[6]4
[7]4dp[7]=4
Item 1
weight=3, value=4
Sweep Direction
w = 7 → 3 (Reverse)
Best dp[7]
4
[Step 1] Initialized 1D table with Item 1 (wt=3, val=4) across capacities 3..7.

2. Needleman-Wunsch (Global) vs. Smith-Waterman (Local) Sequence Alignment

In computational genomics, mismatches carry penalty $-\mu$ and gaps carry penalty $-\delta$ while matches score $+\sigma$:

  • Needleman–Wunsch (Global Alignment): Aligns full strings end-to-end; initializes $dp[i][0] = -i\delta, dp[0][j] = -j\delta$ and returns $dp[n][m]$.
  • Smith–Waterman (Local Alignment): Finds the highest-scoring aligned substrings by adding a $0$ floor to every state transition ($dp[i][j] = \max(0, \dots)$)—allowing the alignment path to start fresh at any $(i, j)$ with score $0$—and returns $\max_{i,j} dp[i][j]$.
flowchart TD Diag["(i-1, j-1)
Match (+0) or Substitute (+1)"] -->|"Diagonal Edge"| Cell["(i, j)"] Up["(i-1, j)
Delete A[i-1] (+1)"] -->|"Vertical Edge"| Cell Left["(i, j-1)
Insert B[j-1] (+1)"] -->|"Horizontal Edge"| Cell

Hardware Reality — Myers' Bit-Parallel $O(N M / 64)$ Edit Distance & SETH Lower Bound

Can general Edit Distance or LCS be solved in $O(N^{2 - \epsilon})$ time? Backurs and Indyk (STOC 2015) proved that any $O(N^{2 - \epsilon})$ algorithm for Edit Distance or LCS would refute the Strong Exponential Time Hypothesis (SETH)! In practice, production libraries (like agrep, ripgrep, and Edlib) use Myers' Bit-Parallel Algorithm (1999), encoding the horizontal and vertical $\in \{-1, 0, +1\}$ differences along columns into 64-bit integers and computing 64 rows in $O(1)$ bitwise ALU operations ($O(N M / 64)$ time).

3. Complexity Matrix

Sequence Alignment Algorithm Time Complexity Space Complexity Best Use Case
Wagner-Fischer Edit Distance (1D Rolling) O(N * M) O(min(N, M)) General strings when only distance is needed
Ukkonen's Banded Edit Distance ($d \le K$) O(N * K) O(K) Fuzzy search, spell-checkers, near-identical files
Hunt-Szymanski (LCS on Permutation / Sparse Matches) O((N + R) log N) O(N + R) Permutations ($R \le M$) or large alphabets
Myers' Bit-Parallel Edit Distance O(N * M / 64) O(M / 64) High-throughput genomics & CLI search (agrep)

4. Reference C++20 Implementation

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <string_view>
#include <unordered_map>
#include <vector>

// 1. Wagner-Fischer Levenshtein Edit Distance in O(N*M) Time and O(min(N, M)) Space
int edit_distance_space_optimized(std::string_view a, std::string_view b) {
    if (a.size() < b.size()) std::swap(a, b);
    const int n = static_cast<int>(a.size());
    const int m = static_cast<int>(b.size());

    std::vector<int> dp(m + 1);
    for (int j = 0; j <= m; ++j) dp[j] = j;

    for (int i = 1; i <= n; ++i) {
        int prev_diag = dp[0]; // Holds dp[i - 1][j - 1]
        dp[0] = i;             // Base case dp[i][0] = i deletions
        for (int j = 1; j <= m; ++j) {
            int saved_up = dp[j];
            if (a[i - 1] == b[j - 1]) {
                dp[j] = prev_diag;
            } else {
                dp[j] = 1 + std::min({ saved_up, dp[j - 1], prev_diag });
            }
            prev_diag = saved_up;
        }
    }
    return dp[m];
}

// 2. Ukkonen's O(N * K) Banded Edit Distance (Returns distance if <= k, else k + 1)
int banded_edit_distance(std::string_view a, std::string_view b, int k) {
    const int n = static_cast<int>(a.size());
    const int m = static_cast<int>(b.size());
    if (std::abs(n - m) > k) return k + 1;

    constexpr int INF = 1'000'000'000;
    std::vector<int> prev(m + 1, INF), curr(m + 1, INF);
    for (int j = 0; j <= std::min(m, k); ++j) prev[j] = j;

    for (int i = 1; i <= n; ++i) {
        int lo = std::max(1, i - k);
        int hi = std::min(m, i + k);
        curr[lo - 1] = (lo == 1) ? i : INF;

        int row_min = curr[lo - 1];
        for (int j = lo; j <= hi; ++j) {
            int cost = (a[i - 1] == b[j - 1]) ? 0 : 1;
            curr[j] = std::min({
                prev[j] + 1,         // Delete
                curr[j - 1] + 1,     // Insert
                prev[j - 1] + cost   // Match / Substitute
            });
            row_min = std::min(row_min, curr[j]);
        }
        if (hi < m) curr[hi + 1] = INF; // Guard boundary outside band
        if (row_min > k) return k + 1;   // Early exit: entire band exceeded threshold k!
        std::swap(prev, curr);
    }
    return std::min(k + 1, prev[m]);
}

// 3. Hunt-Szymanski O((N + M) log N) LCS when elements of a are distinct
int lcs_distinct_via_lis(const std::vector<int>& a, const std::vector<int>& b) {
    std::unordered_map<int, int> pos_in_a;
    pos_in_a.reserve(a.size() * 2);
    for (int i = 0; i < static_cast<int>(a.size()); ++i) {
        pos_in_a[a[i]] = i;
    }
    std::vector<int> tails;
    for (int x : b) {
        auto it = pos_in_a.find(x);
        if (it == pos_in_a.end()) continue;
        int idx = it->second;
        auto lb = std::lower_bound(tails.begin(), tails.end(), idx);
        if (lb == tails.end()) tails.push_back(idx);
        else *lb = idx;
    }
    return static_cast<int>(tails.size());
}

Production Pitfall — Forgetting to Update dp[0] = i in 1-Row Edit Distance

In Longest Common Subsequence, $dp[i][0] = 0$ for all $i$, so dp[0] stays 0 forever. In Levenshtein Edit Distance, transforming $A[0..i-1]$ into the empty string requires $i$ deletions ($dp[i][0] = i$)! Forgetting to set prev_diag = dp[0]; dp[0] = i; at the start of every outer row loop leaves dp[0] == 0, allowing free deletions of prefixes of $A$ and returning completely wrong distances.

Check yourself Question 1 of 4
Concept Check Deletion-Insertion Edit Distance and LCS Duality recall

If only character **insertions** (cost 1) and **deletions** (cost 1) are allowed (no substitutions), what is the exact minimum edit distance between string $A$ of length $n$ and string $B$ of length $m$ in terms of $|\text{LCS}(A, B)|$?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

When reducing Longest Common Subsequence $\text{LCS}(A, B)$ to Longest Increasing Subsequence (LIS) via the Hunt-Szymanski algorithm on strings that contain **duplicate characters**, we replace each character $B[j]$ with the list of all indices $i$ where $A[i] == B[j]$. In what order must the matching indices $i$ for a single character $B[j]$ be emitted?

Key Takeaways

Wagner-Fischer Edit Distance relaxes an (N+1) x (M+1) DAG where horizontal edges insert, vertical edges delete, and diagonal edges match or substitute.
When only edits up to threshold K are relevant, Ukkonen's banded DP restricts evaluation to |i - j| <= K, cutting time from O(N * M) to O(N * K).
When one sequence has distinct elements (or sparse matching pairs), Hunt-Szymanski reduces LCS to 1D Longest Increasing Subsequence in O(N log N) time.
Under SETH, no strongly subquadratic O(N^(2 - eps)) algorithm exists for general Edit Distance or LCS.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue