Algorithm Search⌘K

Sellers algorithm in Python

O(nm) dynamic program for approximate matching, with the first row fixed to zero.

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

"""Sellers' dynamic program 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[i:j] has edit
distance at most k to the pattern.
"""


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

    Precondition: k >= 0. Cost: Theta(len(pattern) * len(text)) time and
    O(len(pattern)) extra space.
    """
    if k < 0:
        raise ValueError("k must be at least 0")
    m = len(pattern)
    col = list(range(m + 1))  # @line L1
    ends = [0] if col[m] <= k else []  # @line L2
    for j in range(1, len(text) + 1):  # @line L3
        t = text[j - 1]
        prev = col[0]  # @line L4
        col[0] = 0
        for i in range(1, m + 1):  # @line L5
            c = 0 if pattern[i - 1] == t else 1  # @line L6
            new = min(col[i] + 1, col[i - 1] + 1, prev + c)  # @line L7
            prev, col[i] = col[i], new  # @line L8
        if col[m] <= k:  # @line L9
            ends.append(j)
    return ends


def last_row(pattern: str, text: str) -> list[int]:
    """Return C[m][j] for j = 0..len(text): the least edit distance between the
    pattern and a substring of text that ends at j. Cost: Theta(mn)."""
    m = len(pattern)
    col = list(range(m + 1))
    row = [col[m]]
    for t in text:
        prev, col[0] = col[0], 0
        for i in range(1, m + 1):
            new = min(col[i] + 1, col[i - 1] + 1, prev + (pattern[i - 1] != t))
            prev, col[i] = col[i], new
        row.append(col[m])
    return row
test_sellers_algorithm.py 50 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sellers-algorithm/. The authors are listed in prov/TZXPTF.jsonl.

import random

import pytest
from sellers_algorithm import last_row, sellers


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[i:j]) for i in range(j + 1)) <= k
    ]


def test_example() -> None:
    assert last_row("survey", "surgery") == [6, 5, 4, 3, 3, 2, 2, 2]
    assert sellers("survey", "surgery", 2) == [5, 6, 7]
    assert sellers("survey", "surgery", 1) == []
    assert sellers("abc", "abxabcbc", 1) == [2, 3, 5, 6, 7, 8]


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


def test_random_against_brute_force() -> None:
    rng = random.Random(1980)
    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 sellers(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 Sellers algorithm