A suffix tree is a data structure that maintains all suffixes of a string.
Notation
Let be the original string used to construct the suffix tree, with length and character set .
Let denote the -th character of , where .
Let denote the string formed by the -th through -th characters of ; this is called a substring of .
Let be the suffix of beginning at , and let be the prefix of ending at .
Definition
The suffix trie of string is the trie obtained by inserting all suffixes of S into a trie. In a suffix trie, the string corresponding to node x is the concatenation of the characters along the path from the root to x. Let the nodes in the suffix trie corresponding to some suffix of be called suffix nodes.
It is easy to see the advantageous property of a suffix trie: its non-root nodes are exactly able to represent all essentially different nonempty substrings of . However, both the time and space complexities of constructing a suffix trie are , which is unacceptable in many cases, so we introduce the concept of a suffix tree.
If we regard all nodes in the suffix trie with more than one child and all suffix nodes as key points, and define the compressed trie obtained by retaining only the key points and compressing chains formed by non-key points into single edges as the suffix tree. If we regard only all nodes with more than one child and leaf nodes in the suffix trie as key points, and define the compressed trie obtained by retaining only the key points as the implicit suffix tree. It is easy to see that the implicit suffix tree is obtained by compressing the suffix tree further.
In a suffix tree and an implicit suffix tree, each edge corresponds to a string. Each non-root node corresponds to a set of strings: the string passed through from the root to 's parent node , concatenated with any nonempty prefix of the string corresponding to the tree edge from to ; this is called . In an implicit suffix tree, a suffix with no corresponding node is called an implicit suffix.
The figure below shows, from left to right, the suffix trie, suffix tree, and implicit suffix tree constructed from the original string .

Consider inserting the suffixes of into the suffix trie one by one. Starting from the second insertion, each insertion adds at most one node with more than one child and one suffix node, so the suffix tree has at most nodes, which is excellent.
Constructing a Suffix Tree
Algorithm Supporting Dynamic Addition of Characters at the Front
The parent tree of the SAM built from the reversed string is the suffix tree of this string, so we only need to add the characters of the reversed string to the SAM one by one.
Reference Implementation
struct SuffixAutomaton {
int tot, lst;
int siz[N << 1];
int buc[N], id[N << 1];
struct Node {
int len, link;
int ch[26];
} st[N << 1];
SuffixAutomaton() : tot(1), lst(1) {}
void extend(int ch) {
int cur = ++tot, p = lst;
lst = cur;
siz[cur] = 1, st[cur].len = st[p].len + 1;
for (; p && !st[p].ch[ch]; p = st[p].link) st[p].ch[ch] = cur;
if (!p)
st[cur].link = 1;
else {
int q = st[p].ch[ch];
if (st[q].len == st[p].len + 1)
st[cur].link = q;
else {
int pp = ++tot;
st[pp] = st[q];
st[pp].len = st[p].len + 1;
st[cur].link = st[q].link = pp;
for (; p && st[p].ch[ch] == q; p = st[p].link) st[p].ch[ch] = pp;
}
}
}
} SAM;
Algorithm Supporting Dynamic Addition of Characters at the Back
Ukkonen's algorithm is an incremental construction algorithm. We insert each character of string into the tree in order and correctly maintain the current suffix tree after every insertion.
Naive algorithm
First, let us introduce a relatively brute-force construction method. We use the string to demonstrate the construction process.
Initially, create a root node, called node . For each edge, maintain an interval representing the string on that edge as . Also maintain the number of inserted characters , initially .
First insert character . Directly extend an edge from node , label it , and point it to a newly created node. Here is an extremely large value that can be understood as the end of the string; thus, when a new character is inserted, this edge automatically includes the new character.

Next insert character . Similarly, extend an edge from node , label it . Notice that the meaning of the previously extended edge changes automatically: as the end of the string changes, the string it represents changes from to . This is correct, because all previous suffixes have already appeared in the tree as leaf nodes, so we only need to insert the current character at the end of every leaf node.

Next, we insert another character . However, is a substring of the string already inserted, so the existing tree already contains . At this point, we do nothing and record a indicating that is the current longest implicit suffix.

Next insert another . Because the previous was not inserted successfully, we now have , meaning that the suffix to insert is . We search downward from the root for and find that it is also already in the original tree. Again, we do nothing.

Notice that we did not handle the suffixes after . If is an implicit suffix, then for , is also an implicit suffix. Since being an implicit suffix implies that there exists a character such that is a substring of , is also a substring of . By the definition of an implicit suffix tree, also does not appear as a leaf node.
Next we insert . Since , we need to search downward from the root for and find that it is not in the original tree. We need to extend an outgoing edge labeled from the node represented by . However, this node does not actually exist; it lies inside an edge, so we need to split that edge, create a new node, and then extend the outgoing edge we want to create from the new node. The insertion now succeeds, so set , because is no longer an implicit suffix.

Next, because changed, we repeat this process until an implicit suffix appears again or (the latter occurs in this example).

The construction process ends.
The worst-case complexity of searching downward from the root and inserting by brute force each time is , so the total complexity is .
Suffix Links
The naive algorithm is slow mainly because each extend must find the insertion position of the longest implicit suffix starting from the root. Therefore, consider recording this position. First, use a pair to describe the current longest implicitly contained suffix . Following the outgoing edge from node that starts with for a length of should reach a position that uniquely represents a string. Each time a new character is inserted, we only need to search from the position described by and .
Now we only need to update when . If , we only need to set , because the next suffix to insert has length of the one just inserted. Otherwise, let the substring corresponding to be . We need to find a node corresponding to and set .
First, we have the following lemma: for any non-leaf, non-root node in an implicit suffix tree, there exists another non-leaf node in the tree such that is the substring corresponding to with its first character removed.
Proof. Let denote the string formed by removing the first character from . By the definition of an implicit suffix tree, there exist two different characters such that both and are substrings of . Therefore, and are also substrings of , so also corresponds to a branching key point in the suffix trie. Thus, in the implicit suffix trie there is a such that . QED.
By this lemma, define , called the Suffix Link of x. Thus must exist. We only need to find for every non-root, non-leaf node in the implicit suffix tree.
Ukkonen's Algorithm
The overall process of Ukkonen's algorithm is as follows:
To construct the implicit suffix tree, add the characters of from front to back. Suppose the root node is , the implicit suffix tree for has been constructed, and the suffix links have been maintained. The longest implicit suffix of is , whose position in the tree is . Let ; we now need to add character x. At this point, character must be appended to the end of every suffix of . Since every explicit suffix corresponds to a leaf node in the tree and the right endpoint of its parent edge is , no maintenance is needed. Thus, we only need to consider the effect of appending to the end of an implicit suffix on the shape of the tree. First consider ; there are two cases:
- The position already has a transition for . The shape of the suffix tree does not change. Since has already appeared in the suffix tree, for , will also appear in the suffix tree. We only need to set , without making any modification.
- The position has no transition for . If happens to be a node in the tree, add an outgoing edge to this node; otherwise, split the node, add a new node at this position, and add an outgoing edge at the new node. For , we do not know what effect will have on the shape of the suffix tree, so we must continue considering . To find the position of in the suffix tree: if is not , use the suffix link and set ; otherwise, set . Finally set and repeat this process.
Each step takes only constant time, and the algorithm stops after all characters have been inserted, so the time complexity is .
Since Ukkonen's algorithm can only construct the implicit suffix tree of , and an implicit suffix tree may be less powerful than a suffix tree for some problems, when necessary, a character that has never appeared can be added to the end of , so that all suffixes of S correspond one-to-one with all leaves of the tree.
Reference Implementation
struct SuffixTree {
int ch[M + 5][RNG + 1], st[M + 5], len[M + 5], link[M + 5];
int s[N + 5];
int now{1}, rem{0}, n{0}, tot{1};
SuffixTree() { len[0] = inf; }
int new_node(int s, int le) {
++tot;
st[tot] = s;
len[tot] = le;
return tot;
}
void extend(int x) {
s[++n] = x;
++rem;
for (int lst{1}; rem;) {
while (rem > len[ch[now][s[n - rem + 1]]])
rem -= len[now = ch[now][s[n - rem + 1]]];
int &v{ch[now][s[n - rem + 1]]}, c{s[st[v] + rem - 1]};
if (!v || x == c) {
lst = link[lst] = now;
if (!v)
v = new_node(n, inf);
else
break;
} else {
int u{new_node(st[v], rem - 1)};
ch[u][c] = v;
ch[u][x] = new_node(n, inf);
st[v] += rem - 1;
len[v] -= rem - 1;
lst = link[lst] = v = u;
}
if (now == 1)
--rem;
else
now = link[now];
}
}
} Tree;
Applications
Every node on a suffix tree has a path to the root that is a nonempty substring of , which is useful for handling many string problems.
The DFS order of a suffix tree is the suffix array. A subtree of a suffix tree therefore corresponds to an interval in the suffix array. The longest common prefix of two suffixes in a suffix tree is the LCA of their corresponding leaf nodes. Therefore, the conclusion about the height of a suffix array can be understood as saying that the LCA of several nodes in the tree equals the LCA of the nodes with the smallest and largest DFS orders.
Examples
Luogu P3804【Template】Suffix Automaton (SAM)
Problem statement:
Given a string containing only lowercase letters.
Find the maximum, over all substrings of whose number of occurrences is not , of the number of occurrences multiplied by the length of the substring.
Solution
Build the implicit suffix tree after inserting a terminator. Every path starting from the root forms a substring. The number of occurrences of an explicit suffix is the number of leaf nodes in the corresponding node's subtree. Implicit suffixes need not be considered, because the number of occurrences of an implicit suffix equals that of the explicit suffix corresponding to the first node reached by going downward, and that explicit suffix is necessarily longer. Therefore, traverse the entire tree, find the number of leaves in each node's subtree and the length of the path from each node to the root. If the number of leaves is , update the answer. The complexity is .
Reference Code
#include <iostream>
#include <string>
using namespace std;
constexpr int N(1e6), M(2 * N), inf(1e7), RNG{26};
struct SuffixTree {
int ch[M + 5][RNG + 1], st[M + 5], len[M + 5], link[M + 5];
int s[N + 5];
int now{1}, rem{0}, n{0}, tot{1};
SuffixTree() { len[0] = inf; }
int new_node(int s, int le) {
++tot;
st[tot] = s;
len[tot] = le;
return tot;
}
void extend(int x) {
s[++n] = x;
++rem;
for (int lst{1}; rem;) {
while (rem > len[ch[now][s[n - rem + 1]]])
rem -= len[now = ch[now][s[n - rem + 1]]];
int &v{ch[now][s[n - rem + 1]]}, c{s[st[v] + rem - 1]};
if (!v || x == c) {
lst = link[lst] = now;
if (!v)
v = new_node(n, inf);
else
break;
} else {
int u{new_node(st[v], rem - 1)};
ch[u][c] = v;
ch[u][x] = new_node(n, inf);
st[v] += rem - 1;
len[v] -= rem - 1;
lst = link[lst] = v = u;
}
if (now == 1)
--rem;
else
now = link[now];
}
}
pair<long long, int> search(int u, int dep = 0) {
if (st[u] + len[u] >= n) return {0, 1};
dep += len[u];
long long ans{0};
int ys{0};
for (int i{0}; i <= RNG; ++i)
if (ch[u][i]) {
auto res = search(ch[u][i], dep);
ans = max(ans, res.first);
ys += res.second;
}
if (ys > 1) ans = max(ans, 1LL * dep * ys);
return {ans, ys};
}
} T;
string s;
int main() {
cin >> s;
for (int i = 0; i < s.size(); ++i) T.extend(s[i] - 'a' + 1);
T.extend(0);
cout << T.search(1).first << endl;
return 0;
}
CF235C Cyclical Quest
Problem statement: Given a lowercase main string and query strings, find the sum, for each query string , of the numbers of occurrences of all its cyclic shifts in the main string.
Solution
Build the implicit suffix tree after inserting a terminator.
Enumerate the current cyclic shift and record the length of the prefix that can be found in the tree.
Repeat a process similar to Ukkonen's algorithm, recording the current matching position . Each time try to insert the next character; if successful, continue inserting; otherwise, exit the loop.
If a cyclic shift is successfully matched and that cyclic shift has not appeared before, update the answer.
Then, when switching to the next cyclic shift, remove the first character of the currently matched substring. This is exactly equivalent to setting . Of course, if , it is enough to directly set .
The complexity is .
Reference Code
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
constexpr int N(1e6);
struct SuffixTree {
static constexpr int N{2 * ::N}, RNG{26}, inf = 1e7;
int ch[N + 5][RNG + 1];
int st[N + 5], len[N + 5], link[N + 5], s[::N + 5];
int now{1}, rem{0}, tot{1}, n{0};
int cnt[N + 5], vis[N + 5];
SuffixTree() { len[0] = inf; }
void clear() {
memset(ch, 0, sizeof ch);
now = tot = 1;
rem = n = 0;
}
int new_node(int s, int le) {
++tot;
st[tot] = s;
len[tot] = le;
return tot;
}
void extend(int x) {
s[++n] = x;
++rem;
for (int lst{1}; rem;) {
while (rem > len[ch[now][s[n - rem + 1]]])
rem -= len[now = ch[now][s[n - rem + 1]]];
int &v{ch[now][s[n - rem + 1]]}, c{s[st[v] + rem - 1]};
if (!v || x == c) {
lst = link[lst] = now;
if (!v)
v = new_node(n, inf);
else
break;
} else {
int u{new_node(st[v], rem - 1)};
ch[u][c] = v;
ch[u][x] = new_node(n, inf);
st[v] += rem - 1;
len[v] -= rem - 1;
lst = link[lst] = v = u;
}
if (now == 1)
--rem;
else
now = link[now];
}
}
void init(int u) {
if (len[u] > 1e6) return cnt[u] = 1, void();
for (int i{0}; i <= RNG; ++i)
if (ch[u][i]) init(ch[u][i]), cnt[u] += cnt[ch[u][i]];
}
long long test(const char *t, int m) {
static int time{0};
++time;
int now{1}, rem{0}, o{0};
long long ans{0};
for (int i{1}; i <= m; ++i) {
while (o < i + m - 1) {
while (rem >= len[ch[now][t[o - rem + 1]]])
rem -= len[now = ch[now][t[o - rem + 1]]];
int v{ch[now][t[o - rem + 1]]}, c{s[st[v] + rem]};
if (v && c == t[o + 1]) {
++o;
++rem;
} else {
break;
}
}
if (o == i + m - 1 && vis[ch[now][t[o - rem + 1]]] != time)
ans += cnt[ch[now][t[o - rem + 1]]],
vis[ch[now][t[o - rem + 1]]] = time;
if (now == 1)
--rem;
else
now = link[now];
}
return ans;
}
} T;
string s;
int main() {
cin >> s;
for (int i = 0; i < s.size(); ++i) T.extend(s[i] - 'a' + 1);
T.extend(0);
T.init(1);
int pw;
cin >> pw;
while (pw--) {
cin >> s;
int n = s.size();
for (auto &ch : s) ch += 1 - 'a';
s = " " + s + s;
cout << T.test(s.data(), n) << "\n";
}
return 0;
}
References
- 2021 National Training Team paper "Constructing a Suffix Tree" by Chen Chenxin
- Cool Suffix Tree Magic - EternalAlexander's Blog