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
# 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