DSA DSA In Depth
0%
lesson myers-diffhirschberglcs

Myers O(ND) Shortest Edit Script & Hirschberg's Linear-Space Alignment

7 min read Module 4.2: Suffix Structures, Sequence Alignment & Data Compression Updated Oct 23, 2026
📄 Cheat Sheet
Hirschberg's Divide-and-Conquer Linear-Space Sequence Alignment & LCS
David Powell Hirschberg's Divide-and-Conquer Linear-Space Sequence Alignment & LCS Watch on YouTube ↗

Derives the edit graph, k = x - y diagonals, greedy snakes, and how Myers explores D = 0, 1, 2... edits in O((N + M)D) time.

0:00 / --:--

1. Myers Edit Graph: Diagonals $k = x - y$ & Greedy Snakes

Given sequences $A[0..N-1]$ and $B[0..M-1]$, consider the grid of vertices $(x, y) \in [0, N] \times [0, M]$. Define diagonal $k$ as the set of grid points satisfying $k = x - y$:

  • Deletion from $A$ ($(x, y) \to (x + 1, y)$): Increases $x$ by $1$, moving from diagonal $k - 1$ to diagonal $k$.
  • Insertion from $B$ ($(x, y) \to (x, y + 1)$): Increases $y$ by $1$, moving from diagonal $k + 1$ to diagonal $k$.
  • Match / Snake ($(x, y) \to (x + 1, y + 1)$ when $A[x] == B[y]$): Increases both $x$ and $y$ by $1$, sliding down the same diagonal $k = x - y$ at $0$ edit cost!

Let $V_d[k]$ be the maximum $x$-coordinate reachable on diagonal $k$ using at most $d$ edits. Because $d$ edits starting from $(0, 0)$ (diagonal $0$) change the diagonal by $\pm 1$ on each edit, after $d$ edits only diagonals $k \in [-d, +d]$ with $k \equiv d \pmod 2$ are reachable! To reach diagonal $k$ at step $d$, we must have taken either a vertical insertion from diagonal $k + 1$ (preserving $x = V_{d-1}[k + 1]$) or a horizontal deletion from diagonal $k - 1$ (advancing $x = V_{d-1}[k - 1] + 1$), followed by a greedy diagonal slide (snake) while $A[x] == B[y]$:

$$x_{\text{start}} = \begin{cases} V_{d-1}[k + 1] & \text{if } k = -d \text{ or } (k \neq d \text{ and } V_{d-1}[k - 1] < V_{d-1}[k + 1]) \\ V_{d-1}[k - 1] + 1 & \text{otherwise} \end{cases}$$

2. Hirschberg's Linear-Space Divide-and-Conquer ($O(N + M)$ Memory)

Whether using classical LCS DP or Myers diff, reconstructing the actual edit script naively requires storing the history of DP rows ($O(N \cdot M)$ or $O((N + M)D)$ space). Dan Hirschberg (1975) showed how to reconstruct the exact optimal alignment in $O(N + M)$ space while only doubling the runtime constant:

  1. Split $A$ at its midpoint $x_{\text{mid}} = \lfloor N / 2 \rfloor$.
  2. Run 2-row space-optimized LCS/edit-distance forward on $A[0..x_{\text{mid}}-1]$ against $B[0..M-1]$ to obtain row $L_1[0..M]$.
  3. Run 2-row space-optimized LCS/edit-distance backwards on the reversed suffix $A[x_{\text{mid}}..N-1]$ against $B[0..M-1]$ to obtain row $L_2[0..M]$.
  4. Find $y^* = \arg\max_{0 \le y \le M} (L_1[y] + L_2[y])$ (or $\arg\min$ for edit distance). Then $(x_{\text{mid}}, y^*)$ is guaranteed to lie on an optimal alignment path! Recurse on $(A[0..x_{\text{mid}}-1],\; B[0..y^*-1])$ and $(A[x_{\text{mid}}..N-1],\; B[y^*..M-1])$.

Because the subproblem areas sum to $\frac{N}{2} y^* + \frac{N}{2}(M - y^*) = \frac{1}{2} NM$, the geometric series of work across all recursion levels is $NM(1 + \frac{1}{2} + \frac{1}{4} + \dots) = 2NM = O(NM)$ time in $O(N + M)$ space!

Diff / Alignment Algorithm Time Complexity Space Complexity Best Use Case
Standard Wagner-Fischer / LCS DP $\Theta(N \cdot M)$ $\Theta(N \cdot M)$ Short strings ($N, M \le 2000$)
Hirschberg Divide-and-Conquer LCS $\Theta(N \cdot M)$ ($2\times$ work) $\Theta(N + M)$ linear space! Long sequences with high divergence ($D \approx N$)
Myers Shortest Edit Script (Greedy) $O((N + M)D)$ $O(D^2)$ trace or $O(D)$ w/ Hirschberg Source code & document diffs where $D \ll N$
Myers Parity & Furthest-Reaching Point Invariant

Every edit step $(x, y) \to (x+1, y)$ or $(x, y) \to (x, y+1)$ changes $k = x - y$ by $+1$ or $-1$, while diagonal snake steps $(x, y) \to (x+1, y+1)$ leave $k = x - y$ unchanged. Consequently, after $d$ edits, only diagonals $k \in \{-d, -d+2, \dots, d\}$ are reachable, and maximizing $x$ on diagonal $k$ simultaneously maximizes $y = x - k$ and $x + y = 2x - k$ (total progress toward $(N, M)$).

Single-Array Overwrite Enabled by Parity Alternation

Because step $d$ only writes to diagonals $k \equiv d \pmod 2$ and only reads from diagonals $k \pm 1 \equiv d - 1 \pmod 2$, the even and odd diagonals never collide! Thus $V_d[k]$ can overwrite a single flat array V[k + offset] in-place without needing separate V_prev and V_curr buffers.

3. Visual Trace: Myers Edit Graph & Hirschberg Midpoint Split

Myers Edit Graph (k = x - y Diagonals) Red = 1-cost Ins/Del; Green = 0-cost Snake Hirschberg O(N+M) Space Midpoint Split Subproblem 1 Subproblem 2 (N/2, y*) Area halves each level: NM + NM/2 + NM/4... = 2NM
flowchart TD A["Init V[1] = 0; offset = N + M"] --> B["For d = 0 .. N + M"] B --> C["For k = -d to +d step 2"] C --> D{"k == -d or (k != d and V[k-1] < V[k+1])?"} D -- "Yes (Down / Insert)" --> E["x = V[k + 1]"] D -- "No (Right / Delete)" --> F["x = V[k - 1] + 1"] E --> G["y = x - k; while x < N and y < M and A[x] == B[y]: x++, y++"] F --> G G --> H["V[k] = x; if x >= N and y >= M return d"] H --> C
Myers O(ND) Edit Graph (k-Diagonals) & Huffman Tree / LZ77 Sliding-Window Deflate
O((N+M)D) Myers Diff · Huffman + LZ77

Explore Eugene Myers’ k = x − y diagonal edit graph with zero-cost diagonal snakes (used by git diff), bottom-up min-heap Huffman optimal prefix code trees, and LZ77 sliding-window (distance, length) back-references.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Myers Edit Graph (A="ABCABBA", B="CBABAC"): diagonal k = x − y. At d=0 edits, start at (0,0); A[0]='A' != B[0]='C' so snake length = 0.
Myers Edit Graph (k = x − y Diagonals) — Edit Distance Budget d = 0 Path: (0,0) → (0,0) Diagonal Snake: while A[x]==B[y] → (x+1, y+1) at Cost 0!
[0]k=-1: —
[1]k=0: V[0]=0
[2]k=+1: —
Edit Budget d
d = 0
Active Diagonal k
k = x − y = 0
Furthest Reach
(0,0) → (0,0)
Complexity
O((N + M) · D)
[Step 1] d = 0: start at (0,0) on diagonal k = 0. No immediate diagonal match.

4. Production Implementation: Myers $O(ND)$ Shortest Edit Distance & Hirschberg LCS

#include <algorithm>
#include <string>
#include <string_view>
#include <vector>

// 1. Myers O((N + M) * D) Shortest Edit Script Distance (Ins + Del, where Match = 0)
int myers_edit_distance(std::string_view a, std::string_view b) {
    const int n = static_cast<int>(a.size());
    const int m = static_cast<int>(b.size());
    const int max_d = n + m;
    if (max_d == 0) return 0;

    std::vector<int> v(2 * max_d + 3, 0);
    const int offset = max_d + 1;

    for (int d = 0; d <= max_d; ++d) {
        for (int k = -d; k <= d; k += 2) {
            int x;
            if (k == -d || (k != d && v[offset + k - 1] < v[offset + k + 1])) {
                x = v[offset + k + 1];     // Vertical step (Insert from B)
            } else {
                x = v[offset + k - 1] + 1; // Horizontal step (Delete from A)
            }
            int y = x - k;
            while (x < n && y < m && a[x] == b[y]) {
                ++x;
                ++y;
            }
            v[offset + k] = x;
            if (x >= n && y >= m) return d;
        }
    }
    return max_d;
}

// 2. Hirschberg's O(N * M) Time, O(N + M) Space LCS String Reconstruction
std::vector<int> lcs_row(std::string_view a, std::string_view b, bool rev) {
    const int n = static_cast<int>(a.size());
    const int m = static_cast<int>(b.size());
    std::vector<int> dp(m + 1, 0), nxt(m + 1, 0);
    for (int i = 0; i < n; ++i) {
        char ca = rev ? a[n - 1 - i] : a[i];
        for (int j = 0; j < m; ++j) {
            char cb = rev ? b[m - 1 - j] : b[j];
            nxt[j + 1] = (ca == cb) ? (dp[j] + 1) : std::max(dp[j + 1], nxt[j]);
        }
        dp.swap(nxt);
    }
    return dp;
}

std::string hirschberg_lcs(std::string_view a, std::string_view b) {
    if (a.empty() || b.empty()) return "";
    if (a.size() == 1) {
        return (b.find(a[0]) != std::string_view::npos) ? std::string(a) : "";
    }
    size_t mid = a.size() / 2;
    auto l1 = lcs_row(a.substr(0, mid), b, false);
    auto l2 = lcs_row(a.substr(mid), b, true);

    size_t best_k = 0;
    int best_val = -1;
    for (size_t k = 0; k <= b.size(); ++k) {
        int val = l1[k] + l2[b.size() - k];
        if (val > best_val) {
            best_val = val;
            best_k = k;
        }
    }
    return hirschberg_lcs(a.substr(0, mid), b.substr(0, best_k)) +
           hirschberg_lcs(a.substr(mid), b.substr(best_k));
}
Initializing V[offset + 1] = 0 for $d = 0$ in Myers Diff

At $d = 0$, $k = 0$ satisfies $k == -d$, so the branch if (k == -d || ...) reads x = V[offset + 1]. Because V is zero-initialized, V[offset + 1] == 0 starts the $d = 0$ snake at $(x, y) = (0, 0)$ without any special-case code! However, make sure your array V is sized at least 2 * (N + M) + 3 so that reading V[offset + k + 1] at $k = +d$ or V[offset + k - 1] at $k = -d$ never indexes out of bounds.

Check yourself Question 1 of 4
Concept Check Myers Edit Graph Diagonal Invariant recall

In Myers' diff algorithm on sequences $A[0..N-1]$ and $B[0..M-1]$, diagonal $k$ is defined as the set of grid vertices $(x, y)$ with $k = x - y$.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

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

If sequences $A$ (length $N$) and $B$ (length $M$) have Longest Common Subsequence length $L = |\text{LCS}(A, B)|$, what is the exact Myers Shortest Edit Script distance $D$, and on which diagonal must the final path terminate?

Key Takeaways

Myers' algorithm finds the Shortest Edit Script in O((N + M)D) time by expanding edit count d = 0..D and sliding greedily along 0-cost match diagonals k = x - y.
Because each insertion or deletion changes k = x - y by +-1 while matches keep k constant, step d only visits diagonals with k === d (mod 2), allowing in-place updates of V[k].
Hirschberg's divide-and-conquer algorithm reconstructs optimal alignments in O(N + M) linear memory by meeting a forward 2-row DP and a reverse 2-row DP at the midpoint row N/2.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue