Skip to content

Commit d73e1e4

Browse files
committed
Add Suffix Automaton algorithm in strings
1 parent 25a312d commit d73e1e4

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

‎strings/suffix_automaton.py‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""
2+
Suffix Automaton (SAM) for String Processing.
3+
4+
Reference: https://en.wikipedia.org/wiki/Suffix_automaton
5+
Reference: https://cp-algorithms.com/string/suffix-automaton.html
6+
7+
A Suffix Automaton is the minimal Deterministic Finite Automaton (DFA) that recognizes
8+
all suffixes (and substrings) of a given string in O(N) time and O(N) space.
9+
"""
10+
11+
12+
class State:
13+
"""
14+
State (node) in a Suffix Automaton.
15+
"""
16+
17+
def __init__(self, length: int = 0, link: int = -1) -> None:
18+
self.length: int = length
19+
self.link: int = link
20+
self.next: dict[str, int] = {}
21+
22+
23+
class SuffixAutomaton:
24+
"""
25+
Suffix Automaton data structure.
26+
27+
>>> sam = SuffixAutomaton("abacaba")
28+
>>> sam.contains("abac")
29+
True
30+
>>> sam.contains("caba")
31+
True
32+
>>> sam.contains("xyz")
33+
False
34+
>>> sam.count_distinct_substrings()
35+
21
36+
>>> sam.count_occurrences("aba")
37+
2
38+
>>> sam.count_occurrences("a")
39+
4
40+
>>> SuffixAutomaton("")
41+
Traceback (most recent call last):
42+
...
43+
ValueError: Input string must not be empty.
44+
"""
45+
46+
def __init__(self, string: str) -> None:
47+
if not string:
48+
raise ValueError("Input string must not be empty.")
49+
50+
self.states: list[State] = [State(length=0, link=-1)]
51+
self.last: int = 0
52+
self.string: str = string
53+
54+
for char in string:
55+
self.extend(char)
56+
57+
def extend(self, char: str) -> None:
58+
"""
59+
Extend the Suffix Automaton by appending character char.
60+
Time Complexity: O(1) amortized
61+
"""
62+
curr = len(self.states)
63+
self.states.append(State(length=self.states[self.last].length + 1))
64+
65+
p = self.last
66+
while p != -1 and char not in self.states[p].next:
67+
self.states[p].next[char] = curr
68+
p = self.states[p].link
69+
70+
if p == -1:
71+
self.states[curr].link = 0
72+
else:
73+
q = self.states[p].next[char]
74+
if self.states[p].length + 1 == self.states[q].length:
75+
self.states[curr].link = q
76+
else:
77+
clone = len(self.states)
78+
self.states.append(
79+
State(
80+
length=self.states[p].length + 1,
81+
link=self.states[q].link,
82+
)
83+
)
84+
self.states[clone].next = dict(self.states[q].next)
85+
86+
while p != -1 and self.states[p].next.get(char) == q:
87+
self.states[p].next[char] = clone
88+
p = self.states[p].link
89+
90+
self.states[q].link = clone
91+
self.states[curr].link = clone
92+
93+
self.last = curr
94+
95+
def contains(self, pattern: str) -> bool:
96+
"""
97+
Check if pattern exists as a substring in O(|pattern|) time.
98+
99+
>>> sam = SuffixAutomaton("banana")
100+
>>> sam.contains("nan")
101+
True
102+
>>> sam.contains("apple")
103+
False
104+
"""
105+
curr = 0
106+
for char in pattern:
107+
if char not in self.states[curr].next:
108+
return False
109+
curr = self.states[curr].next[char]
110+
return True
111+
112+
def count_distinct_substrings(self) -> int:
113+
"""
114+
Compute total number of distinct substrings in O(N) time.
115+
116+
>>> sam = SuffixAutomaton("abc")
117+
>>> sam.count_distinct_substrings()
118+
6
119+
>>> SuffixAutomaton("aaaa").count_distinct_substrings()
120+
4
121+
"""
122+
total = 0
123+
for i in range(1, len(self.states)):
124+
total += self.states[i].length - self.states[self.states[i].link].length
125+
return total
126+
127+
def count_occurrences(self, pattern: str) -> int:
128+
"""
129+
Count occurrences of pattern as a substring in the text in O(|pattern|) time.
130+
131+
>>> sam = SuffixAutomaton("banana")
132+
>>> sam.count_occurrences("an")
133+
2
134+
>>> sam.count_occurrences("na")
135+
2
136+
>>> sam.count_occurrences("banana")
137+
1
138+
>>> sam.count_occurrences("xyz")
139+
0
140+
"""
141+
curr = 0
142+
for char in pattern:
143+
if char not in self.states[curr].next:
144+
return 0
145+
curr = self.states[curr].next[char]
146+
147+
# Standard endpos size calculation via suffix link tree
148+
occurrences = [0] * len(self.states)
149+
order = sorted(
150+
range(len(self.states)),
151+
key=lambda i: self.states[i].length,
152+
reverse=True,
153+
)
154+
155+
# Mark initial end positions of prefix states
156+
temp_last = 0
157+
for char in self.string:
158+
temp_last = self.states[temp_last].next[char]
159+
occurrences[temp_last] = 1
160+
161+
# Push endpos sizes up the suffix link tree
162+
for u in order:
163+
if self.states[u].link != -1:
164+
occurrences[self.states[u].link] += occurrences[u]
165+
166+
return occurrences[curr]
167+
168+
169+
if __name__ == "__main__":
170+
import doctest
171+
172+
doctest.testmod()

0 commit comments

Comments
 (0)