Algorithm Search⌘K

Suffix tree from suffix array in Python

Build the suffix tree from the suffix array and LCP array in linear time.

File
suffix_tree_from_suffix_array.py, 106 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
suffix_tree_from_suffix_array.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/suffix-tree-from-suffix-array/. The authors are listed in prov/Q1HEHJ.jsonl.

SENTINEL = "\0"


class Node:
    """A suffix tree node. depth is the string depth; start is the start of a suffix below it.

    The label of the edge into the node is text[start + parent.depth : start + depth].
    leaf is the suffix number for a leaf and -1 for an internal node.
    """

    __slots__ = ("depth", "start", "leaf", "children")

    def __init__(self, depth: int, start: int, leaf: int = -1) -> None:
        self.depth = depth
        self.start = start
        self.leaf = leaf
        self.children: list[Node] = []


def suffix_array(t: str) -> list[int]:
    """A simple suffix array, for the tests and the examples."""
    return sorted(range(len(t)), key=lambda i: t[i:])


def lcp_array(t: str, sa: list[int]) -> list[int]:
    """Kasai's algorithm: lcp[i] is the longest common prefix of the suffixes sa[i-1] and sa[i]; lcp[0] = 0."""
    n = len(t)
    rank = [0] * n
    for i, p in enumerate(sa):
        rank[p] = i
    lcp = [0] * n
    h = 0
    for p in range(n):
        if rank[p] > 0:
            q = sa[rank[p] - 1]
            while p + h < n and q + h < n and t[p + h] == t[q + h]:
                h += 1
            lcp[rank[p]] = h
            if h > 0:
                h -= 1
        else:
            h = 0
    return lcp


def suffix_tree_from_sa(t: str, sa: list[int], lcp: list[int]) -> Node:
    """Build the suffix tree of t from its suffix array and LCP array in O(n) time.

    t must end with a symbol that occurs nowhere else, so that every suffix ends at a leaf.
    """
    n = len(t)
    root = Node(0, 0)
    stack = [root]  # @line L1
    for i in range(n):
        h, last = lcp[i], None  # @line L2
        while stack[-1].depth > h:  # @line L3
            last = stack.pop()
        top = stack[-1]
        if top.depth < h:  # @line L4
            assert last is not None
            u = Node(h, last.start)
            top.children[-1] = u
            u.children.append(last)
            stack.append(u)
            top = u
        v = Node(n - sa[i], sa[i], sa[i])  # @line L5
        top.children.append(v)
        stack.append(v)
    return root  # @line L6


def build(s: str) -> tuple[str, Node]:
    """Append the sentinel to s and build its suffix tree through the suffix array."""
    if SENTINEL in s:
        raise ValueError("the text must not contain the sentinel")
    t = s + SENTINEL
    sa = suffix_array(t)
    return t, suffix_tree_from_sa(t, sa, lcp_array(t, sa))


def edge_label(t: str, parent: Node, child: Node) -> str:
    return t[child.start + parent.depth: child.start + child.depth]


def check_invariant(t: str, root: Node) -> None:
    """Every internal node but the root branches, children are sorted by their first symbol,
    and each leaf spells its suffix."""
    leaves = 0
    stack: list[tuple[Node, str]] = [(root, "")]
    while stack:
        x, path = stack.pop()
        assert len(path) == x.depth
        if x.leaf >= 0:
            assert not x.children and path == t[x.leaf:]
            leaves += 1
            continue
        assert x is root or len(x.children) >= 2, "unary internal node"
        firsts = [edge_label(t, x, c)[0] for c in x.children]
        assert firsts == sorted(firsts) and len(set(firsts)) == len(firsts)
        for c in x.children:
            assert c.depth > x.depth and t.startswith(path, c.start)
            stack.append((c, path + edge_label(t, x, c)))
    assert leaves == len(t)
test_suffix_tree_from_suffix_array.py 60 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/suffix-tree-from-suffix-array/. The authors are listed in prov/Q1HEHJ.jsonl.

import random

import pytest

from suffix_tree_from_suffix_array import Node, build, check_invariant, edge_label, lcp_array, suffix_array


def internal_labels(t: str, root: Node) -> set[str]:
    out: set[str] = set()
    stack: list[tuple[Node, str]] = [(root, "")]
    while stack:
        x, path = stack.pop()
        if x.leaf < 0:
            out.add(path)
            for c in x.children:
                stack.append((c, path + edge_label(t, x, c)))
    return out


def branching_substrings(t: str) -> set[str]:
    """The right-branching substrings of t, plus the empty string: the internal nodes of the suffix tree."""
    follow: dict[str, set[str]] = {}
    for i in range(len(t)):
        for j in range(i, len(t)):
            follow.setdefault(t[i:j], set()).add(t[j])
    return {w for w, f in follow.items() if len(f) >= 2} | {""}


def test_example() -> None:
    t = "banana\0"
    sa = suffix_array(t)
    lcp = lcp_array(t, sa)
    assert sa == [6, 5, 3, 1, 0, 4, 2] and lcp == [0, 0, 1, 3, 0, 0, 2]
    t, root = build("banana")
    check_invariant(t, root)
    assert sorted(internal_labels(t, root)) == ["", "a", "ana", "na"]
    assert [edge_label(t, root, c) for c in root.children] == ["\0", "a", "banana\0", "na"]


def test_edges() -> None:
    for s in ["", "a", "aaaa", "abab", "abcd"]:
        t, root = build(s)
        check_invariant(t, root)
        assert internal_labels(t, root) == branching_substrings(t)
    t, root = build("aaaa")
    assert sorted(internal_labels(t, root)) == ["", "a", "aa", "aaa"]
    with pytest.raises(ValueError):
        build("a\0b")


def test_random_against_naive() -> None:
    rng = random.Random(2004)
    for case in range(600):
        s = "".join(rng.choice("ab" if case % 2 else "abc") for _ in range(rng.randint(0, 25)))
        t, root = build(s)
        check_invariant(t, root)
        assert internal_labels(t, root) == branching_substrings(t)

What the next level needs

  • the tests did not run in this build

Read the full page about Suffix tree from suffix array