Algorithm Search⌘K

DC3 algorithm in Python

Linear-time suffix array construction by recursing on the suffixes at positions not divisible by three.

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

import heapq

# Difference covers modulo v: every residue d mod v is a difference of two members.
COVERS: dict[int, tuple[int, ...]] = {3: (1, 2), 7: (1, 2, 4), 13: (1, 2, 4, 10)}


def is_difference_cover(d: tuple[int, ...], v: int) -> bool:
    return {(a - b) % v for a in d for b in d} == set(range(v))


def _radix_sort(items: list[int], keys: list[list[int]], k: int) -> list[int]:
    """Stable LSD radix sort of items by the tuples keys[i] (all of the same length), digits < k."""
    width = len(keys[items[0]]) if items else 0
    for p in range(width - 1, -1, -1):
        buckets: list[list[int]] = [[] for _ in range(k)]
        for i in items:
            buckets[keys[i][p]].append(i)
        items = [i for b in buckets for i in b]
    return items


def suffix_array(s: str, v: int = 3) -> list[int]:
    """Return the suffix array of s with the difference cover algorithm DC-v."""
    if v not in COVERS:
        raise ValueError(f"no difference cover stored for v = {v}")
    t = [ord(c) + 1 for c in s]
    return _dc(t, max(t, default=0) + 1, v)


def _dc(t: list[int], k: int, v: int) -> list[int]:
    """Suffix array of t, a list of ints in [1, k); positions past the end read as 0."""
    n = len(t)
    if n <= 2 * v:
        return sorted(range(n), key=lambda i: t[i:])
    cover = COVERS[v]
    in_d = [False] * v
    for d in cover:
        in_d[d] = True

    def ch(i: int) -> int:
        return t[i] if i < n else 0

    # sample positions, class by class, including position n if its class is in D
    sample = [i for d in cover for i in range(d, n + 1, v)]  # @line L1
    # sort the samples by their first v characters and name them
    keys: list[list[int]] = [[] for _ in range(n + 1)]
    for i in sample:
        keys[i] = [ch(i + j) for j in range(v)]
    order = _radix_sort(sample, keys, k)  # @line L2
    name = [0] * (n + 1)
    cur, prev = 0, None
    for i in order:
        if keys[i] != prev:
            cur, prev = cur + 1, keys[i]
        name[i] = cur
    rank = [0] * (n + v + 1)  # rank of a sample position; 0 past the end
    if cur < len(sample):  # @line L3
        # recurse on the names written class by class
        sub = [name[i] for i in sample]
        sub_sa = _dc(sub, cur + 1, v)  # @line L4
        for r, j in enumerate(sub_sa):  # @line L5
            rank[sample[j]] = r + 1
    else:
        for i in sample:  # @line L5
            rank[i] = name[i]
    # sort each residue class by (first l characters, rank at offset l)
    lists: list[list[int]] = []
    ck: list[list[int]] = [[] for _ in range(n)]
    for c in range(v):  # @line L6
        l = next(x for x in range(v) if in_d[(c + x) % v])
        cls = list(range(c, n, v))
        if not cls:
            continue
        for i in cls:
            ck[i] = [ch(i + j) for j in range(l)] + [rank[i + l]]
        lists.append(_radix_sort(cls, ck, max(k, n + 2)))
    # offsets l with i + l and j + l both in the sample, for each pair of classes
    off = [[next(x for x in range(v) if in_d[(a + x) % v] and in_d[(b + x) % v])
            for b in range(v)] for a in range(v)]  # @line L7

    class _Key:
        __slots__ = ("i",)

        def __init__(self, i: int) -> None:
            self.i = i

        def __lt__(self, other: "_Key") -> bool:
            i, j = self.i, other.i
            l = off[i % v][j % v]
            for x in range(l):
                if ch(i + x) != ch(j + x):
                    return ch(i + x) < ch(j + x)
            return rank[i + l] < rank[j + l]

    # merge the v sorted classes
    merged = heapq.merge(*([_Key(i) for i in lst] for lst in lists))  # @line L8
    return [key.i for key in merged]


def naive_suffix_array(s: str) -> list[int]:
    return sorted(range(len(s)), key=lambda i: s[i:])
test_dc3_algorithm.py 41 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dc3-algorithm/. The authors are listed in prov/13MGBB.jsonl.

import itertools
import random

import pytest

from dc3_algorithm import COVERS, is_difference_cover, naive_suffix_array, suffix_array


def test_example() -> None:
    s = "abracadabra"
    assert suffix_array(s) == [10, 7, 0, 3, 5, 8, 1, 4, 6, 9, 2]
    assert suffix_array(s, 7) == naive_suffix_array(s)


def test_covers() -> None:
    for v, d in COVERS.items():
        assert is_difference_cover(d, v)
        # no smaller cover exists
        for r in range(1, len(d)):
            assert not any(is_difference_cover(c, v) for c in itertools.combinations(range(v), r))


def test_edges() -> None:
    for s in ["", "a", "aa", "ab", "ba", "aaaaaaaaaaaaaaaaaaaaaaa", "abababababababababab", "zyxwvutsrqponmlkjihgfedcba"]:
        for v in COVERS:
            assert suffix_array(s, v) == naive_suffix_array(s)
    with pytest.raises(ValueError):
        suffix_array("abc", 5)


def test_random_against_naive() -> None:
    rng = random.Random(3003)
    for case in range(600):
        n = rng.randint(0, 80)
        alpha = "ab" if case % 3 == 0 else "abcd"
        s = "".join(rng.choice(alpha) for _ in range(n))
        v = (3, 7, 13)[case % 3]
        assert suffix_array(s, v) == naive_suffix_array(s)

What the next level needs

  • the tests did not run in this build

Read the full page about DC3 algorithm