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
# 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