Apostolico-Giancarlo algorithm in Python
Boyer-Moore variant that records previous match information to bound the total comparisons linearly.
- File
apostolico_giancarlo.py, 91 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/apostolico-giancarlo/. The authors are listed in prov/7XHZHF.jsonl.
from collections.abc import Hashable, Sequence
def suffixes(p: Sequence[Hashable]) -> list[int]:
"""suff[i] = length of the longest common suffix of p[:i+1] and p."""
m = len(p)
suff = [0] * m
if m == 0:
return suff
suff[m - 1] = m
g = f = m - 1
for i in range(m - 2, -1, -1):
if i > g and suff[i + m - 1 - f] < i - g:
suff[i] = suff[i + m - 1 - f]
else:
g = min(g, i)
f = i
while g >= 0 and p[g] == p[g + m - 1 - f]:
g -= 1
suff[i] = f - g
return suff
def good_suffix(p: Sequence[Hashable], suff: list[int]) -> list[int]:
"""Strong good-suffix table; gs[0] is the smallest period of p."""
m = len(p)
gs = [m] * m
j = 0
for i in range(m - 1, -1, -1):
if suff[i] == i + 1:
while j < m - 1 - i:
if gs[j] == m:
gs[j] = m - 1 - i
j += 1
for i in range(m - 1):
gs[m - 1 - suff[i]] = m - 1 - i
return gs
def apostolico_giancarlo(text: Sequence[Hashable], pattern: Sequence[Hashable], stats: dict | None = None) -> list[int]:
"""All shifts s with text[s:s+m] == pattern, in increasing order.
skip[t % m] holds, for a text position t of the current window, the length of the
longest suffix of the pattern known to end at t (0 if unknown).
If stats is a dict, stats["comparisons"] receives the number of character comparisons.
"""
n, m = len(text), len(pattern) # @line L1
if m == 0: # @line L2
if stats is not None:
stats["comparisons"] = 0
return list(range(n + 1))
suff = suffixes(pattern) # @line L3
gs = good_suffix(pattern, suff)
last = {c: k for k, c in enumerate(pattern)}
skip = [0] * m # @line L4
out, comparisons = [], 0
s = 0
while s <= n - m: # @line L5
i = m - 1 # @line L6
while i >= 0: # @line L7
k = skip[(s + i) % m] # @line L8
h = suff[i]
if k > 0: # @line L9
if k > h: # @line L10
i = -1 if h == i + 1 else i - h
break
i -= k # @line L11
if k < h:
break
else:
comparisons += 1 # @line L12
if pattern[i] == text[s + i]:
i -= 1
else:
break
if i < 0: # @line L13
out.append(s)
skip[(s + m - 1) % m] = m
shift = gs[0]
else: # @line L14
skip[(s + m - 1) % m] = m - 1 - i
shift = max(gs[i], i - last.get(text[s + i], -1))
for t in range(s, s + min(shift, m)): # @line L15
skip[t % m] = 0
s += shift # @line L16
if stats is not None:
stats["comparisons"] = comparisons
return out
test_apostolico_giancarlo.py 62 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/apostolico-giancarlo/. The authors are listed in prov/7XHZHF.jsonl.
import itertools
import random
from apostolico_giancarlo import apostolico_giancarlo, good_suffix, suffixes
def oracle(t: str, p: str) -> list[int]:
return [s for s in range(len(t) - len(p) + 1) if t.startswith(p, s)]
def test_example() -> None:
stats: dict = {}
assert apostolico_giancarlo("gcatcgcagagagtatacagtacg", "gcagagag", stats) == [5]
assert stats["comparisons"] == 15
def test_tables() -> None:
p = "gcagagag"
suff = suffixes(p)
assert suff == [1, 0, 0, 2, 0, 4, 0, 8]
assert good_suffix(p, suff) == [7, 7, 7, 2, 7, 4, 7, 1]
def test_edges() -> None:
assert apostolico_giancarlo("abc", "") == [0, 1, 2, 3]
assert apostolico_giancarlo("", "") == [0]
assert apostolico_giancarlo("", "a") == []
assert apostolico_giancarlo("ab", "abc") == []
assert apostolico_giancarlo("a" * 10, "a" * 4) == list(range(7))
assert apostolico_giancarlo("a" * 10, "baaa") == []
def test_periodic_input_is_linear() -> None:
stats: dict = {}
assert apostolico_giancarlo("a" * 1000, "a" * 10, stats) == list(range(991))
assert stats["comparisons"] <= 1000
def test_exhaustive_binary() -> None:
for m in range(1, 5):
for p in itertools.product("ab", repeat=m):
ps = "".join(p)
for n in range(0, 9):
for t in itertools.product("ab", repeat=n):
ts = "".join(t)
stats: dict = {}
assert apostolico_giancarlo(ts, ps, stats) == oracle(ts, ps)
assert 2 * stats["comparisons"] <= 3 * n
def test_random_against_oracle() -> None:
rng = random.Random(1986)
for alphabet in ("a", "ab", "abcd"):
for _ in range(700):
t = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 40)))
p = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 7)))
stats: dict = {}
assert apostolico_giancarlo(t, p, stats) == oracle(t, p)
assert 2 * stats["comparisons"] <= 3 * len(t)
What the next level needs
- the tests did not run in this build