Suffix Automaton: Linear-Time Directed Acyclic Word Graphs for Strings
A Suffix Automaton (SAM)—often referred to as a Directed Acyclic Word Graph (DAWG)—is a powerful deterministic finite automaton (DFA) that encodes all substrings of a string $S$ in optimal $O(N)$ time and space.
While a naive suffix trie contains $O(N^2)$ states and Suffix Trees require complex algorithms (like Ukkonen's algorithm) with high constant memory multipliers, a Suffix Automaton compresses all identical substring state transitions using **endpos equivalence classes**, maintaining at most $2N - 1$ states and $3N - 4$ transitions for any string of length $N$.
Theoretical Foundations: endpos Equivalence and Suffix Links
1. The endpos Set
For any non-empty substring $u$ of string $S$, $\text{endpos}(u)$ is defined as the set of all ending index positions where $u$ appears in $S$. Two substrings $u$ and $v$ are **endpos-equivalent** if and only if $\text{endpos}(u) = \text{endpos}(v)$. A single state in a Suffix Automaton corresponds to exactly one unique endpos equivalence class.
2. Structural Lemmas of endpos Classes
- Subset/Disjoint Property: Two endpos sets are either completely disjoint or one is a strict subset of the other.
- Continuous Lengths: Within a single state $v$, all represented substrings are suffixes of the longest string $\text{len}(v)$, and their lengths form a contiguous integer range $[\text{minlen}(v), \text{len}(v)]$.
- Suffix Link (Parent Pointer): The suffix link $\text{link}(v)$ points to the state corresponding to the longest suffix of substrings in $v$ whose endpos set is strictly larger (i.e., appears in additional positions).
Linear-Time Online Construction Algorithm
A Suffix Automaton is built character by character in amortized $O(N)$ time. When extending the automaton with character $c$:
- Create a new state `cur` with $\text{len}(\text{cur}) = \text{len}(\text{last}) + 1$.
- Ascend the suffix-link chain starting from `last`. For every state $p$ lacking a transition on $c$, add $\text{next}[p][c] = \text{cur}$.
- If no state on the link path had a transition on $c$, set $\text{link}(\text{cur}) = 0$ (root) and set $\text{last} = \text{cur}$.
- If a state $p$ already has transition $q = \text{next}[p][c]$: If $\text{len}(q) == \text{len}(p) + 1$, simply set $\text{link}(\text{cur}) = q$.
- Otherwise (when $\text{len}(q) > \text{len}(p) + 1$), clone state $q$ into a new state `clone` with $\text{len}(\text{clone}) = \text{len}(p) + 1$, copy $q$'s transitions, redirect $\text{link}(q)$ and $\text{link}(\text{cur})$ to `clone`, and redirect transitions pointing to $q$ along $p$'s link path to `clone`.
C++ Implementation Blueprint
#include <vector>
#include <string>
#include <map>
struct State {
int len = 0;
int link = -1;
std::map<char, int> next;
};
class SuffixAutomaton {
private:
std::vector<State> st;
int last = 0;
public:
SuffixAutomaton(int max_len) {
st.reserve(2 * max_len);
st.push_back(State()); // State 0 is the root
}
void extend(char c) {
int cur = st.size();
st.push_back(State());
st[cur].len = st[last].len + 1;
int p = last;
while (p != -1 && !st[p].next.count(c)) {
st[p].next[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = st.size();
st.push_back(State());
st[clone].len = st[p].len + 1;
st[clone].next = st[q].next;
st[clone].link = st[q].link;
while (p != -1 && st[p].next[c] == q) {
st[p].next[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
bool containsSubstring(const std::string& pattern) const {
int curr = 0;
for (char c : pattern) {
if (!st[curr].next.count(c)) return false;
curr = st[curr].next.at(c);
}
return true;
}
};Classic Problems Solved via Suffix Automata
- Counting Distinct Substrings: Summing $(\text{len}(u) - \text{len}(\text{link}(u)))$ across all states in exact $O(N)$ linear time.
- Pattern Matching & Occurrence Frequency: Finding whether a pattern of length $M$ exists in $O(M)$ time and calculating total match counts by pushing state frequencies down the suffix link tree.
- Longest Common Substring (LCS) of Multiple Strings: Traversing the automaton with candidate strings, tracking maximum matching length per transition in $O(\sum |S_i|)$.
- Lexicographically K-th Substring Query: Performing depth-first state counting on DAG paths to query the $k$-th smallest substring in $O(K)$.
- Bioinformatics Sequence Analysis: Indexing massive genomic DNA strands (FASTA) to detect repeat units and tandem variations without memory thrashing.