DC3 (skew) algorithm in Python
Linear-time suffix array construction by recursing on the suffixes at positions not divisible by 3.
- File
dc3_skew_algorithm.py, 86 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/dc3-skew-algorithm/. The authors are listed in prov/M5ZHTB.jsonl.
from collections.abc import Sequence
def _radix_pass(items: list[int], key: list[int], offset: int, k: int) -> list[int]:
"""Stable counting sort of items by key[x + offset], with keys in 0 .. k."""
count = [0] * (k + 2)
for x in items:
count[key[x + offset] + 1] += 1
for b in range(k + 1):
count[b + 1] += count[b]
out = [0] * len(items)
for x in items:
out[count[key[x + offset]]] = x
count[key[x + offset]] += 1
return out
def _dc3(t: list[int], n: int, k: int) -> list[int]:
"""Suffix array of t[0 .. n-1] with symbols in 1 .. k and t[n] = t[n+1] = t[n+2] = 0.
The recursion is on a string of length about 2n/3, so its depth is O(log n).
"""
if n <= 1:
return list(range(n))
n0, n1, n2 = (n + 2) // 3, (n + 1) // 3, n // 3
n02 = n0 + n2
sample = [i for i in range(n + (n0 - n1)) if i % 3 != 0] # @line L1
for offset in (2, 1, 0): # @line L2
sample = _radix_pass(sample, t, offset, k)
name, last = 0, None
rank = [0] * (n02 + 3) # rank[i // 3] for i = 1 mod 3, rank[i // 3 + n0] for i = 2 mod 3
for p in sample: # @line L3
triple = (t[p], t[p + 1], t[p + 2])
if triple != last:
name, last = name + 1, triple
rank[p // 3 if p % 3 == 1 else p // 3 + n0] = name
if name < n02: # @line L4
reduced = rank[:n02] + [0, 0, 0] # @line L5
sa12 = _dc3(reduced, n02, name) # @line L6
for r, x in enumerate(sa12):
rank[x] = r + 1
else:
sa12 = [0] * n02 # @line L7
for x in range(n02):
sa12[rank[x] - 1] = x
sa0 = _radix_pass([3 * x for x in sa12 if x < n0], t, 0, k) # @line L8
def pos12(x: int) -> int:
return 3 * x + 1 if x < n0 else 3 * (x - n0) + 2
out: list[int] = []
a, b = n0 - n1, 0 # skip the dummy sample suffix when n = 1 mod 3
while a < n02 and b < n0: # @line L9
x, j = sa12[a], sa0[b]
i = pos12(x)
if x < n0:
left = (t[i], rank[x + n0])
right = (t[j], rank[j // 3])
else:
left = (t[i], t[i + 1], rank[x - n0 + 1])
right = (t[j], t[j + 1], rank[j // 3 + n0])
if left <= right:
out.append(i)
a += 1
else:
out.append(j)
b += 1
out.extend(pos12(x) for x in sa12[a:])
out.extend(sa0[b:])
return out # @line L10
def dc3(text: Sequence) -> list[int]:
"""Suffix array of text by the DC3 (skew) algorithm of Karkkainen and Sanders.
Symbols need a total order; they are coded 1 .. sigma and the text is padded
with three zeros, which act as end markers, so no sentinel is part of the
output and a proper prefix sorts first. Positions are 0-based.
"""
n = len(text)
code = {c: r + 1 for r, c in enumerate(sorted(set(text)))}
t = [code[c] for c in text] + [0, 0, 0]
return _dc3(t, n, len(code))
test_dc3_skew_algorithm.py 46 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dc3-skew-algorithm/. The authors are listed in prov/M5ZHTB.jsonl.
import random
from dc3_skew_algorithm import dc3
def texts(seed: int, count: int = 900, maxn: int = 40) -> list:
rng = random.Random(seed)
out: list = ["", "a", "aa", "ab", "ba", "aaa", "aba"]
for n in range(1, 25):
out += ["a" * n, "ab" * n, "aab" * n, "abc" * n]
for _ in range(count):
sigma = rng.choice([1, 2, 4])
out.append("".join(rng.choice("abcd"[:sigma]) for _ in range(rng.randrange(maxn + 1))))
return out
def naive_sa(t) -> list[int]:
return sorted(range(len(t)), key=lambda i: t[i:])
def test_examples() -> None:
assert dc3("banana") == [5, 3, 1, 0, 4, 2]
assert dc3("mississippi") == [10, 7, 4, 1, 0, 9, 8, 6, 3, 5, 2]
assert dc3("yabbadabbado") == naive_sa("yabbadabbado")
assert dc3("") == []
def test_against_naive_sort() -> None:
for t in texts(1):
assert dc3(t) == naive_sa(t), t
def test_integer_alphabets() -> None:
rng = random.Random(2)
for _ in range(300):
t = [rng.randrange(rng.choice([1, 2, 4, 100])) for _ in range(rng.randrange(60))]
assert dc3(t) == naive_sa(t)
def test_long_texts() -> None:
rng = random.Random(3)
for t in ["a" * 3000, "ab" * 1500 + "a", "".join(rng.choice("ab") for _ in range(3000))]:
assert dc3(t) == naive_sa(t)
What the next level needs
- the tests did not run in this build