Algorithm Search⌘K

Kasai's algorithm in Python

Computes the LCP array in linear time by reusing the LCP value between consecutive positions.

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

from collections.abc import Sequence


def kasai(text: Sequence, sa: Sequence[int]) -> list[int]:
    """Return the LCP array of text, given its suffix array sa.

    Precondition: sa lists the 0-based start positions of the n nonempty
    suffixes of text in increasing lexicographic order (no sentinel; a proper
    prefix sorts first). The result has lcp[0] = 0 and lcp[i] = the length of
    the longest common prefix of the suffixes sa[i - 1] and sa[i].
    Symbols are only tested for equality. Time O(n), extra space O(n).
    """
    n = len(text)
    rank = [0] * n
    for i in range(n):  # @line L1
        rank[sa[i]] = i
    lcp = [0] * n
    h = 0  # @line L2
    for p in range(n):  # @line L3
        if rank[p] == 0:  # @line L4
            h = 0
            continue
        q = sa[rank[p] - 1]  # @line L5
        while p + h < n and q + h < n and text[p + h] == text[q + h]:  # @line L6
            h += 1
        lcp[rank[p]] = h  # @line L7
        if h > 0:  # @line L8
            h -= 1
    return lcp


def phi_lcp(text: Sequence, sa: Sequence[int]) -> list[int]:
    """The Phi variant: compute PLCP in text order with Phi[sa[i]] = sa[i - 1], then permute it."""
    n = len(text)
    phi = [-1] * n
    for i in range(1, n):
        phi[sa[i]] = sa[i - 1]
    plcp = [0] * n
    h = 0
    for p in range(n):
        q = phi[p]
        if q < 0:
            h = 0
            continue
        while p + h < n and q + h < n and text[p + h] == text[q + h]:
            h += 1
        plcp[p] = h
        if h > 0:
            h -= 1
    return [plcp[sa[i]] if i > 0 else 0 for i in range(n)]
test_kasai_algorithm.py 62 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/kasai-algorithm/. The authors are listed in prov/0Z6PM0.jsonl.

import random

from kasai_algorithm import kasai, phi_lcp


def naive_sa(t):
    return sorted(range(len(t)), key=lambda i: t[i:])


def naive_lcp_array(t):
    sa = naive_sa(t)
    out = []
    for i in range(len(t)):
        if i == 0:
            out.append(0)
            continue
        a, b = t[sa[i - 1] :], t[sa[i] :]
        k = 0
        while k < len(a) and k < len(b) and a[k] == b[k]:
            k += 1
        out.append(k)
    return out


def texts(seed: int, count: int = 700, maxn: int = 16) -> list[str]:
    rng = random.Random(seed)
    out = ["", "a", "aa", "ab", "ba"]
    for n in range(1, 14):
        out += ["a" * n, "ab" * n, "aab" * n, "abaab" * (n // 2 + 1)]
    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 test_examples() -> None:
    assert kasai("banana", [5, 3, 1, 0, 4, 2]) == [0, 1, 3, 0, 0, 2]
    t = "mississippi"
    assert kasai(t, naive_sa(t)) == [0, 1, 1, 4, 0, 0, 1, 0, 2, 1, 3]


def test_edges() -> None:
    assert kasai("", []) == []
    assert kasai("x", [0]) == [0]
    assert kasai("aaaaa", [4, 3, 2, 1, 0]) == [0, 1, 2, 3, 4]
    assert kasai([2, 0, 2, 0], naive_sa([2, 0, 2, 0])) == [0, 1, 0, 2]


def test_against_naive() -> None:
    for t in texts(11):
        sa = naive_sa(t)
        want = naive_lcp_array(t)
        assert kasai(t, sa) == want
        assert phi_lcp(t, sa) == want


def test_long_unary_is_linear() -> None:
    t = "a" * 3000
    assert kasai(t, list(range(2999, -1, -1))) == list(range(3000))

What the next level needs

  • the tests did not run in this build

Read the full page about Kasai's algorithm