Algorithm Search⌘K

Manber-Myers algorithm in Python

Builds the suffix array by prefix doubling in O(n log n) time.

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

from collections.abc import Sequence
from typing import Any


def manber_myers(s: Sequence[Any]) -> list[int]:
    """Return the suffix array of s by prefix doubling with bucket placement.

    The alphabet is general ordered: symbols are compared only in the first
    sort. No sentinel is appended; positions are 0-based, and a suffix that is
    a prefix of another comes first. Time O(n log n) in the worst case, extra
    space O(n) integers.
    """
    n = len(s)
    if n == 0:
        return []
    sa = sorted(range(n), key=lambda i: s[i])  # @line L1
    b = [0] * n  # b[i] = the first rank of the bucket of position i
    for r in range(1, n):  # @line L2
        same = s[sa[r]] == s[sa[r - 1]]
        b[sa[r]] = b[sa[r - 1]] if same else r
    buckets = len(set(b))
    h = 1  # @line L3
    while h < n and buckets < n:  # @line L4
        nxt = list(range(n))  # @line L5: nxt[x] = the next free slot of bucket x
        new = [0] * n
        for i in range(n - 1, n - h - 1, -1):  # @line L6: empty second half first
            new[nxt[b[i]]] = i
            nxt[b[i]] += 1
        for r in range(n):  # @line L7: the others, in the order of S[j + h ..]
            j = sa[r] - h
            if j >= 0:
                new[nxt[b[j]]] = j
                nxt[b[j]] += 1
        sa = new  # @line L8

        def second(i: int) -> int:
            return b[i + h] if i + h < n else -1

        nb = [0] * n  # @line L9: new buckets by (old bucket, second half)
        for r in range(1, n):
            p, q = sa[r - 1], sa[r]
            same = b[p] == b[q] and second(p) == second(q)
            nb[q] = nb[p] if same else r
        b = nb  # @line L10
        buckets = len(set(b))
        h *= 2
    return sa  # @line L11
test_manber_myers_algorithm.py 40 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/manber-myers-algorithm/. The authors are listed in prov/D2EGK8.jsonl.

import random

from manber_myers_algorithm import manber_myers


def naive_sa(s: str) -> list[int]:
    return sorted(range(len(s)), key=lambda i: s[i:])


def test_example() -> None:
    assert manber_myers("mississippi") == [10, 7, 4, 1, 0, 9, 8, 6, 3, 5, 2]
    assert manber_myers("banana") == [5, 3, 1, 0, 4, 2]


def test_edges() -> None:
    assert manber_myers("") == []
    assert manber_myers("z") == [0]
    assert manber_myers("aaaaa") == [4, 3, 2, 1, 0]
    assert manber_myers("ababab") == [4, 2, 0, 5, 3, 1]
    assert manber_myers("ba") == [1, 0]
    assert manber_myers([5, 2, 5, 2, 9]) == [1, 3, 0, 2, 4]


def test_long_periodic() -> None:
    for s in ("a" * 513, "ab" * 300, "abaababaabaab" * 20):
        assert manber_myers(s) == naive_sa(s)


def test_random_against_naive() -> None:
    rng = random.Random(1990)
    for trial in range(1500):
        sigma = "acgt"[: rng.choice([1, 2, 4])]
        n = rng.randint(0, 40)
        s = "".join(rng.choice(sigma) for _ in range(n))
        if trial % 30 == 0:
            s = rng.choice(["a" * n, "ab" * (n // 2), ""])
        assert manber_myers(s) == naive_sa(s)

What the next level needs

  • the tests did not run in this build

Read the full page about Manber-Myers algorithm