Algorithm Search⌘K

Landau-Vishkin algorithm in Python

Computes furthest reaching diagonal points with longest-common-extension queries in O(kn) time.

File
landau_vishkin_algorithm.py, 119 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
landau_vishkin_algorithm.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/landau-vishkin-algorithm/. The authors are listed in prov/2PT2TT.jsonl.

"""The Landau-Vishkin diagonal algorithm for approximate string matching.

Edit model: unit-cost insertion, deletion and substitution (Levenshtein),
no transpositions. Indices are 0-based. The result is the sorted list of end
positions j in 0..len(text) such that some substring text[h:j] has edit
distance at most k to the pattern.

Longest common extensions come from a suffix array of pattern + sep + text +
sep, its LCP array (Kasai) and a sparse table for range minima. All loops are
iterative.
"""

NONE = -(10**18)  # stands for "no cell of the diagonal has a value <= e"


class LCE:
    """Longest common extension of a suffix of a and a suffix of b."""

    def __init__(self, a: str, b: str) -> None:
        syms = sorted(set(a) | set(b))
        code = {c: r + 2 for r, c in enumerate(syms)}
        s = [code[c] for c in a] + [1] + [code[c] for c in b] + [0]
        self.off = len(a) + 1
        n = len(s)
        rank = s[:]
        sa = list(range(n))
        h = 1
        while True:  # prefix doubling: sort by (rank[i], rank[i + h])
            key = [rank[i] * (n + 2) + (rank[i + h] + 1 if i + h < n else 0) for i in range(n)]
            sa.sort(key=key.__getitem__)
            new = [0] * n
            for t in range(1, n):
                new[sa[t]] = new[sa[t - 1]] + (key[sa[t]] != key[sa[t - 1]])
            rank = new
            if rank[sa[-1]] == n - 1:
                break
            h *= 2
        self.rank = rank
        lcp = [0] * n  # lcp[t] = lcp of suffixes sa[t - 1] and sa[t]
        q = 0
        for i in range(n):
            if rank[i] == 0:
                q = 0
                continue
            j = sa[rank[i] - 1]
            while i + q < n and j + q < n and s[i + q] == s[j + q]:
                q += 1
            lcp[rank[i]] = q
            if q:
                q -= 1
        self.table = [lcp]
        w = 1
        while 2 * w <= n:
            prev = self.table[-1]
            self.table.append([min(prev[t], prev[t + w]) for t in range(n - 2 * w + 1)])
            w *= 2

    def query(self, i: int, j: int) -> int:
        """Length of the longest common prefix of a[i:] and b[j:]."""
        x, y = self.rank[i], self.rank[self.off + j]
        if x > y:
            x, y = y, x
        x += 1  # minimum of lcp[x..y]
        lev = (y - x + 1).bit_length() - 1
        row = self.table[lev]
        return min(row[x], row[y - (1 << lev) + 1])


def landau_vishkin(pattern: str, text: str, k: int) -> list[int]:
    """Return the end positions of the approximate matches of pattern in text.

    Precondition: k >= 0. Cost: O((k + 1)(n + k)) constant-time extensions
    after O((n + m) log^2 (n + m)) preprocessing, O(n + m) space for the rows.
    """
    if k < 0:
        raise ValueError("k must be at least 0")
    m, n = len(pattern), len(text)
    lce = LCE(pattern, text)  # @line L1
    lo = -min(k, m)
    prev: list[int] = []  # prev[d - lo] = L[d][e - 1]
    for e in range(k + 1):  # @line L2
        cur = [NONE] * (n - lo + 1)
        for d in range(max(-e, -m), n + 1):  # @line L3
            x = d - lo
            r = max(0, -d)  # @line L4
            if e > 0:  # @line L5
                if prev[x] != NONE:
                    r = max(r, prev[x] + 1)
                if x > 0 and prev[x - 1] != NONE:
                    r = max(r, prev[x - 1])
                if x + 1 < len(prev) and prev[x + 1] != NONE:
                    r = max(r, prev[x + 1] + 1)
            r = min(r, m, n - d)  # @line L6
            if r < m and r + d < n:  # @line L7
                r += lce.query(r, r + d)
            cur[x] = r
        prev = cur
    return [m + d for d in range(max(lo, -m), n - m + 1) if prev[d - lo] == m]  # @line L8


def diagonal_table(pattern: str, text: str, k: int) -> dict[tuple[int, int], int]:
    """Return L[d][e] for e = 0..k and all diagonals d with a cell of value <= e."""
    m, n = len(pattern), len(text)
    lce = LCE(pattern, text)
    table: dict[tuple[int, int], int] = {}
    for e in range(k + 1):
        for d in range(max(-e, -m), n + 1):
            r = max(0, -d)
            for cand in (table.get((d, e - 1), NONE) + 1, table.get((d - 1, e - 1), NONE),
                         table.get((d + 1, e - 1), NONE) + 1):
                r = max(r, cand)
            r = min(r, m, n - d)
            if r < m and r + d < n:
                r += lce.query(r, r + d)
            table[(d, e)] = r
    return table
test_landau_vishkin_algorithm.py 85 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/landau-vishkin-algorithm/. The authors are listed in prov/2PT2TT.jsonl.

import random

import pytest
from landau_vishkin_algorithm import LCE, diagonal_table, landau_vishkin


def edit_distance(a: str, b: str) -> int:
    d = [[i + j if i * j == 0 else 0 for j in range(len(b) + 1)] for i in range(len(a) + 1)]
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] != b[j - 1]))
    return d[len(a)][len(b)]


def brute(pattern: str, text: str, k: int) -> list[int]:
    return [
        j for j in range(len(text) + 1)
        if min(edit_distance(pattern, text[h:j]) for h in range(j + 1)) <= k
    ]


def sellers_table(p: str, t: str) -> list[list[int]]:
    c = [[i if j == 0 else 0 for j in range(len(t) + 1)] for i in range(len(p) + 1)]
    for i in range(1, len(p) + 1):
        for j in range(1, len(t) + 1):
            c[i][j] = min(c[i - 1][j] + 1, c[i][j - 1] + 1, c[i - 1][j - 1] + (p[i - 1] != t[j - 1]))
    return c


def test_example() -> None:
    assert landau_vishkin("GATAA", "CAGATAAGAGAA", 1) == [6, 7, 8, 12]
    assert landau_vishkin("GATAA", "CAGATAAGAGAA", 0) == [7]
    assert landau_vishkin("survey", "surgery", 2) == [5, 6, 7]


def test_edges() -> None:
    assert landau_vishkin("", "", 0) == [0]
    assert landau_vishkin("", "ab", 0) == [0, 1, 2]
    assert landau_vishkin("ab", "", 1) == []
    assert landau_vishkin("ab", "", 2) == [0]
    assert landau_vishkin("ab", "xyz", 5) == [0, 1, 2, 3]
    with pytest.raises(ValueError):
        landau_vishkin("a", "a", -1)


def test_lce() -> None:
    rng = random.Random(7)
    for _ in range(200):
        a = "".join(rng.choice("ab") for _ in range(rng.randint(1, 10)))
        b = "".join(rng.choice("ab") for _ in range(rng.randint(1, 10)))
        s = LCE(a, b)
        for i in range(len(a)):
            for j in range(len(b)):
                q = 0
                while i + q < len(a) and j + q < len(b) and a[i + q] == b[j + q]:
                    q += 1
                assert s.query(i, j) == q


def test_table_is_furthest_row() -> None:
    rng = random.Random(1989)
    for alphabet in ("ab", "acgt"):
        for _ in range(150):
            p = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 6)))
            t = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 10)))
            k = rng.randint(0, 4)
            c = sellers_table(p, t)
            table = diagonal_table(p, t, k)
            for e in range(k + 1):
                for d in range(-len(p), len(t) + 1):
                    rows = [i for i in range(len(p) + 1) if 0 <= i + d <= len(t) and c[i][i + d] <= e]
                    assert table.get((d, e)) == (max(rows) if rows else None)


def test_random_against_brute_force() -> None:
    rng = random.Random(1986)
    for alphabet in ("ab", "acgt"):
        for _ in range(400):
            p = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 6)))
            t = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 12)))
            k = rng.randint(0, 4)
            assert landau_vishkin(p, t, k) == brute(p, t, k)

What the next level needs

  • the tests did not run in this build

Read the full page about Landau-Vishkin algorithm