Suffix Tree

A suffix tree is a data structure that maintains all suffixes of a string.

Notation

Let SS be the original string used to construct the suffix tree, with length nn and character set Σ\Sigma.

Let S[i]S[i] denote the ii-th character of SS, where 1≤i≤n1 \le i \le n.

Let S[l,r]S [l, r] denote the string formed by the ll-th through rr-th characters of SS; this is called a substring of SS.

Let S[i,n]S [i, n] be the suffix of SS beginning at ii, and let S[1,i]S [1, i] be the prefix of SS ending at ii.

Definition

The suffix trie of string SS 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 SS 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 SS. However, both the time and space complexities of constructing a suffix trie are O(n2)O(n^2), 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 xx corresponds to a set of strings: the string passed through from the root to xx's parent node faxfa_x, concatenated with any nonempty prefix of the string corresponding to the tree edge from faxfa_x to xx; this is called strxstr_x. 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 cabab\mathtt{cabab}.

Figure: suffix tree1

Consider inserting the suffixes of SS 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 2n2n 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 SS 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 abbbc\texttt {abbbc} to demonstrate the construction process.

Initially, create a root node, called node 00. For each edge, maintain an interval [l,r][l,r] representing the string on that edge as S[l,r]S[l,r]. Also maintain the number of inserted characters mm, initially 00.

First insert character a\texttt a. Directly extend an edge from node 00, label it [1,∞][1,\infty], and point it to a newly created node. Here ∞\infty 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.

Figure: suffix tree2

Next insert character b\texttt b. Similarly, extend an edge from node 00, label it [2,∞⁡][2,\infty⁡]. Notice that the meaning of the previously extended edge [1,∞][1,\infty] changes automatically: as the end of the string changes, the string it represents changes from a\texttt a to ab\texttt {ab}. 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.

Figure: suffix tree3

Next, we insert another character b\texttt b. However, b\texttt b is a substring of the string already inserted, so the existing tree already contains b\texttt b. At this point, we do nothing and record a kk indicating that S[k,m]S[k,m] is the current longest implicit suffix.

Figure: suffix tree4

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

Figure: suffix tree5

Notice that we did not handle the suffixes after kk. If S[k,m]S[k,m] is an implicit suffix, then for l>kl>k, S[l,m]S[l,m] is also an implicit suffix. Since S[k,m]S[k,m] being an implicit suffix implies that there exists a character cc such that S[k,m]+cS[k, m] + c is a substring of SS, S[l,m]+cS [ l, m] + c is also a substring of SS. By the definition of an implicit suffix tree, S[l,m]S[ l, m] also does not appear as a leaf node.

Next we insert c\texttt c. Since k=3k=3, we need to search downward from the root for bbc\texttt {bbc} and find that it is not in the original tree. We need to extend an outgoing edge labeled [5,∞][5,\infty] from the node represented by bb\texttt {bb}. 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 k→k+1k\to k+1, because S[k,m]S[k,m] is no longer an implicit suffix.

Figure: suffix tree6

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

Figure: suffix tree7

The construction process ends.

The worst-case complexity of searching downward from the root and inserting by brute force each time is O(n)O(n), so the total complexity is O(n2)O(n^2).

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 (now,rem)(now,rem) to describe the current longest implicitly contained suffix S[k,m]S[k,m]. Following the outgoing edge from node nownow that starts with S[m−rem+1]S[m-rem+1] for a length of remrem 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 nownow and remrem.

Now we only need to update (now,rem)(now,rem) when k→k+1k\to k + 1. If now=0now=0, we only need to set rem→rem−1rem \to rem-1, because the next suffix to insert has length −1-1 of the one just inserted. Otherwise, let the substring corresponding to strnowstr_{now} be S[l,r]S[l,r]. We need to find a node now′now' corresponding to S[l+1,r]S[l+1,r] and set now→now′now\to now'.

First, we have the following lemma: for any non-leaf, non-root node xx in an implicit suffix tree, there exists another non-leaf node yy in the tree such that strystr_y is the substring corresponding to strxstr_x with its first character removed.

Proof. Let ss denote the string formed by removing the first character from strxstr_x. By the definition of an implicit suffix tree, there exist two different characters c1,c2c_1,c_2 such that both strx+c1str_x + c1 and strx+c2str_x + c_2 are substrings of SS. Therefore, s+c1s + c_1 and s+c2s + c_2 are also substrings of SS, so ss also corresponds to a branching key point in the suffix trie. Thus, in the implicit suffix trie there is a yy such that stry=sstr_y=s. QED.

By this lemma, define Link⁡(x)=y\operatorname{Link}(x)=y, called the Suffix Link of x. Thus now′=Link⁡(now)now'=\operatorname{Link}(now) must exist. We only need to find Link⁡\operatorname{Link} 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 SS from front to back. Suppose the root node is 00, the implicit suffix tree for S[1,m]S[1, m] has been constructed, and the suffix links have been maintained. The longest implicit suffix of S[1,m]S [1, m] is S[k,m]S [k, m], whose position in the tree is (now,rem)(now, rem). Let S[m+1]=xS [m + 1] = x; we now need to add character x. At this point, character xx must be appended to the end of every suffix of S[1,m]S [1, m]. Since every explicit suffix corresponds to a leaf node in the tree and the right endpoint of its parent edge is ∞\infty, no maintenance is needed. Thus, we only need to consider the effect of appending xx to the end of an implicit suffix on the shape of the tree. First consider S[k,m]S [k, m]; there are two cases:

  1. The position (now,rem)(now, rem) already has a transition for xx. The shape of the suffix tree does not change. Since S[k,m+1]S [k, m+1] has already appeared in the suffix tree, for l>kl > k, S[l,m+1]S [ l, m + 1] will also appear in the suffix tree. We only need to set rem→rem+1rem\to rem + 1, without making any modification.
  2. The position (now,rem)(now, rem) has no transition for xx. If (now,rem)(now, rem) happens to be a node in the tree, add an outgoing edge xx to this node; otherwise, split the node, add a new node at this position, and add an outgoing edge xx at the new node. For l>kl > k, we do not know what effect S[l,m]S [ l, m] will have on the shape of the suffix tree, so we must continue considering S[k+1,m]S [k + 1, m]. To find the position of S[k+1,m]S [k + 1, m] in the suffix tree: if nownow is not 00, use the suffix link and set now=Link⁡(now)now = \operatorname{Link}(now); otherwise, set rem→rem−1rem\to rem − 1. Finally set k→k+1k\to k + 1 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 O(n)O(n).

Since Ukkonen's algorithm can only construct the implicit suffix tree of SS, 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 SS, 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 SS, 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 SS containing only lowercase letters.

Find the maximum, over all substrings of SS whose number of occurrences is not 11, 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 >1>1, update the answer. The complexity is O(∣S∣∣Σ∣)O(|S||\Sigma|).

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 SS and nn query strings, find the sum, for each query string xix_i, 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 (now,rem)(now,rem). 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 now→Link⁡(now)now \to \operatorname{Link}(now). Of course, if now=1now=1, it is enough to directly set rem→rem−1rem\to rem-1.

The complexity is O(∣S∣∣Σ∣+∑∣xi∣)O(|S||\Sigma|+\sum|x_i|).

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

  1. 2021 National Training Team paper "Constructing a Suffix Tree" by Chen Chenxin
  2. Cool Suffix Tree Magic - EternalAlexander's Blog

This content comes from OI Wiki: Suffix Tree. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.