mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-28 21:45:27 +08:00
* Add Suffix Automaton algorithm in strings * Use descriptive variable names for algorithms-keeper * Refactor State class to use dataclass and update count_occurrences time complexity in SuffixAutomaton * Apply suggestion from @cclauss --------- Co-authored-by: Christian Clauss <cclauss@me.com>
178 lines
5.1 KiB
Python
178 lines
5.1 KiB
Python
"""
|
|
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()
|