The compressed trie of all suffixes of s\$$ (a unique sentinel keeps every suffix a leaf): n + 1n\ge 2(l, r)O(n)O(n^2)$ substrings. Every substring is a root path, so many questions become tree navigation. In contests prefer the suffix automaton (its link tree is the suffix tree of the reversed string) or SA + LCP (the LCP interval tree below); Ukkonen is here for completeness and tested.

QuestionMethodCost
is a substring; occurrences of walk from the root; leaves under the arrival point ( output)
longest repeated substringdeepest internal node
distinct substrings of sum of edge lengths of the tree of s\$$ minus the n + 1$$
longest common substring of stringsgeneralised tree (distinct separators); deepest node with leaves of every colour
longest palindromic substringgeneralised tree of and + LCA
maximal repeats; tandem repeatsinternal nodes with left-diverse leaves; Gusfield–Stoye
longest common extensionLCA of two leaves after
matching statisticsone walk with suffix links
ConstructionDirectionOnlineNote
Weiner (1973)right to leftprepend memory
McCreight (1976)longest to shortest suffixnohard
Ukkonen (1995)left to rightappendmost taught; fiddly
Farach (1997)divide and conquernointeger alphabets, theoretical
from SA + LCPnoeasy: monotonic stack
suffix treesuffix arraysuffix automaton
nodes integers
memory constanthigh ( per node)lowestmedium
build difficultyhardesteasyeasiest
onlineUkkonen onlynoyes
lexicographic orderyesnaturalneeds work
all substringsyesvia LCP intervalsnatural

Ukkonen. Phase extends the implicit tree of to . Three tricks make it linear: edge-label compression with leaf edges ending at a shared “current end” (extending all leaves is free: rule 1); suffix links from the node of to the node of , so the next insertion point is reached in amortised ; rule 3 (“showstopper”): if the next character already follows the current point, it follows every shorter suffix too, so the phase stops. The active point encodes where the next insertion goes.

struct SuffixTree {                                         // Ukkonen; edges [l, r) into s; append a unique sentinel yourself
    struct Node { int l, r, par, link; map<char, int> nxt; Node(int l = 0, int r = 0, int p = -1) : l(l), r(r), par(p), link(-1) {} int len() const { return r - l; } };
    string s; int n; vector<Node> t; pair<int, int> ptr{0, 0};   // (node, position on the edge into node)
    int child(int v, char c) { auto it = t[v].nxt.find(c); return it == t[v].nxt.end() ? -1 : it->second; }
    pair<int, int> go(pair<int, int> st, int l, int r) {    // walk down by s[l, r) from state st; {-1,-1} if impossible
        while (l < r) {
            if (st.second == t[st.first].len()) { int v = child(st.first, s[l]); if (v == -1) return {-1, -1}; st = {v, 0}; }
            else {
                if (s[t[st.first].l + st.second] != s[l]) return {-1, -1};
                if (r - l < t[st.first].len() - st.second) return {st.first, st.second + r - l};
                l += t[st.first].len() - st.second; st.second = t[st.first].len();
            }
        }
        return st;
    }
    int split(pair<int, int> st) {                          // make the point explicit, return its node
        auto [v, pos] = st;
        if (pos == t[v].len()) return v;
        if (pos == 0) return t[v].par;
        Node old = t[v]; int id = sz(t); t.push_back(Node(old.l, old.l + pos, old.par));
        t[old.par].nxt[s[old.l]] = id; t[id].nxt[s[old.l + pos]] = v;
        t[v].par = id; t[v].l += pos;
        return id;
    }
    int getLink(int v) {
        if (t[v].link != -1) return t[v].link;
        if (t[v].par == -1) return 0;
        int to = getLink(t[v].par);
        return t[v].link = split(go({to, t[to].len()}, t[v].l + (t[v].par == 0), t[v].r));
    }
    void extend(int pos) {
        while (true) {
            auto np = go(ptr, pos, pos + 1);
            if (np.first != -1) { ptr = np; return; }       // rule 3
            int mid = split(ptr), leaf = sz(t); t.push_back(Node(pos, n, mid)); t[mid].nxt[s[pos]] = leaf;
            ptr.first = getLink(mid); ptr.second = t[ptr.first].len();
            if (!mid) break;
        }
    }
    SuffixTree(const string& str) : s(str), n(sz(str)) { t.push_back(Node()); for (int i = 0; i < n; i++) extend(i); }
    ll distinctSubstringsWithSentinel() const { ll r = 0; for (int v = 1; v < sz(t); v++) r += t[v].len(); return r; }
    int countOccurrences(const string& p) {                 // leaves below the locus of p
        pair<int, int> cur = {0, 0};
        for (char ch : p) {
            if (cur.second == t[cur.first].len()) { int u = child(cur.first, ch); if (u == -1) return 0; cur = {u, 1}; }
            else { if (s[t[cur.first].l + cur.second] != ch) return 0; cur.second++; }
        }
        vector<int> stck = {cur.first}; int leaves = 0;
        while (!stck.empty()) { int u = stck.back(); stck.pop_back(); if (t[u].nxt.empty()) leaves++; for (auto& [c, w] : t[u].nxt) stck.push_back(w); }
        return leaves;
    }
};
 
vector<array<int, 3>> lcpIntervals(const vector<int>& lcp) {   // (depth, lb, rb) of every internal node, lcp from kasai (lcp[0] = 0)
    int n = sz(lcp); vector<array<int, 3>> res; vector<pair<int, int>> st = {{0, 0}};   // (depth, lb)
    for (int i = 1; i <= n; i++) {
        int cur = i < n ? lcp[i] : 0, lb = i - 1;
        while (st.back().first > cur) { auto [d, l] = st.back(); st.pop_back(); res.push_back({d, l, i - 1}); lb = l; }
        if (st.back().first < cur) st.push_back({cur, lb});
    }
    res.push_back({0, 0, n - 1});                           // the root
    return res;
}