Suffix Automaton
R
"""
Suffix Automaton (SAM) for String Processing.
Reference: https://en.wikipedia.org/wiki/Suffix_automaton
Reference: https://cp-algorithms.com/string/suffix-automaton.html
A Suffix Automaton is the minimal Deterministic Finite Automaton (DFA) that recognizes
all suffixes (and substrings) of a given string in O(N) time and O(N) space.
"""
from dataclasses import dataclass, field
@dataclass
class State:
"""
State (node) in a Suffix Automaton.
"""
length: int = 0
link: int = -1
next: dict[str, int] = field(default_factory=dict)
class SuffixAutomaton:
"""
Suffix Automaton data structure.
>>> sam = SuffixAutomaton("abacaba")
>>> sam.contains("abac")
True
>>> sam.contains("caba")
True
>>> sam.contains("xyz")
False
>>> sam.count_distinct_substrings()
21
>>> sam.count_occurrences("aba")
2
>>> sam.count_occurrences("a")
4
>>> SuffixAutomaton("")
Traceback (most recent call last):
...
ValueError: Input string must not be empty.
"""
def __init__(self, string: str) -> None:
if not string:
raise ValueError("Input string must not be empty.")
self.states: list[State] = [State(length=0, link=-1)]
self.last: int = 0
self.string: str = string
for char in string:
self.extend(char)
def extend(self, char: str) -> None:
"""
Extend the Suffix Automaton by appending character char.
Time Complexity: O(1) amortized
"""
curr = len(self.states)
self.states.append(State(length=self.states[self.last].length + 1))
prev_state = self.last
while prev_state != -1 and char not in self.states[prev_state].next:
self.states[prev_state].next[char] = curr
prev_state = self.states[prev_state].link
if prev_state == -1:
self.states[curr].link = 0
else:
next_state = self.states[prev_state].next[char]
if self.states[prev_state].length + 1 == self.states[next_state].length:
self.states[curr].link = next_state
else:
clone = len(self.states)
self.states.append(
State(
length=self.states[prev_state].length + 1,
link=self.states[next_state].link,
)
)
self.states[clone].next = dict(self.states[next_state].next)
while (
prev_state != -1
and self.states[prev_state].next.get(char) == next_state
):
self.states[prev_state].next[char] = clone
prev_state = self.states[prev_state].link
self.states[next_state].link = clone
self.states[curr].link = clone
self.last = curr
def contains(self, pattern: str) -> bool:
"""
Check if pattern exists as a substring in O(|pattern|) time.
>>> sam = SuffixAutomaton("banana")
>>> sam.contains("nan")
True
>>> sam.contains("apple")
False
"""
curr = 0
for char in pattern:
if char not in self.states[curr].next:
return False
curr = self.states[curr].next[char]
return True
def count_distinct_substrings(self) -> int:
"""
Compute total number of distinct substrings in O(N) time.
>>> sam = SuffixAutomaton("abc")
>>> sam.count_distinct_substrings()
6
>>> SuffixAutomaton("aaaa").count_distinct_substrings()
4
"""
total = 0
for state in self.states[1:]:
total += state.length - self.states[state.link].length
return total
def count_occurrences(self, pattern: str) -> int:
"""
Count occurrences of pattern as a substring in the text in O(N + |pattern|) time
>>> sam = SuffixAutomaton("banana")
>>> sam.count_occurrences("an")
2
>>> sam.count_occurrences("na")
2
>>> sam.count_occurrences("banana")
1
>>> sam.count_occurrences("xyz")
0
"""
curr = 0
for char in pattern:
if char not in self.states[curr].next:
return 0
curr = self.states[curr].next[char]
# Standard endpos size calculation via suffix link tree
occurrences = [0] * len(self.states)
order = sorted(
range(len(self.states)),
key=lambda state_index: self.states[state_index].length,
reverse=True,
)
# Mark initial end positions of prefix states
temp_last = 0
for char in self.string:
temp_last = self.states[temp_last].next[char]
occurrences[temp_last] = 1
# Push endpos sizes up the suffix link tree
for state_index in order:
if self.states[state_index].link != -1:
occurrences[self.states[state_index].link] += occurrences[state_index]
return occurrences[curr]
if __name__ == "__main__":
import doctest
doctest.testmod()