The sorted order of all suffixes, with the LCP array ( = longest common prefix of the suffixes at ranks and ), answers most substring questions with far less memory than a suffix tree. Construction: prefix doubling with radix sort , or SA-IS ; LCP by Kasai ; LCP of any two suffixes by RMQ.

s = banana     sa  = [5, 3, 1, 0, 4, 2]   (a, ana, anana, banana, na, nana)
               lcp = [-, 1, 3, 0, 0, 2]
QueryMethodCost
does / how often occurtwo binary searches on the SA
number of distinct substrings
longest repeated substring
longest common substring of two stringsconcatenate with a separator; max lcp between neighbours from different sides
longest common substring of stringsdistinct separators, sliding window over the SA covering all colours, window min-lcp
LCP of any two suffixes, substring comparisonsparse table over lcp: build
-th distinct substring in lexicographic orderwalk the SA, each rank contributes new substrings
distinct substrings of many ranges offline sweep + suffix automaton + link-cut tree

The LCP array implicitly encodes the suffix tree: internal nodes are the maximal SA intervals whose minimum lcp equals the node’s string depth, enumerable with a monotonic stack (Monotonic Stack); so SA + LCP has the power of a suffix tree at a fraction of the memory and effort.

ConstructionTimeLinesWhen
sort suffixes naively3
prefix doubling (KMR) + sort~20
prefix doubling + radix sort~25, the contest default
DC3 / skew (Kärkkäinen–Sanders)~60
SA-IS (Nong–Zhang–Chan)~50fastest in practice
suffix arraysuffix automatonsuffix tree
memory ints, smallest
build easy, SA-IS, about 30 lines (Ukkonen), hard
online appendnoyesyes
lexicographic ordernaturalneeds workyes
contest defaultwhen order mattersusuallyrarely
vector<int> suffixArray(const string& str) {               // prefix doubling with counting sort, O(n log n)
    string s = str + char(0); int n = sz(s), A = 256;      // input must not contain '\0'
    vector<int> p(n), c(n), cnt(max(A, n), 0), pn(n), cn(n);
    for (char ch : s) cnt[(unsigned char)ch]++;
    for (int i = 1; i < A; i++) cnt[i] += cnt[i - 1];
    for (int i = n - 1; i >= 0; i--) p[--cnt[(unsigned char)s[i]]] = i;
    int cls = 1; c[p[0]] = 0;
    for (int i = 1; i < n; i++) { if (s[p[i]] != s[p[i - 1]]) cls++; c[p[i]] = cls - 1; }
    for (int h = 0; (1 << h) < n; h++) {                   // sort cyclic shifts of length 2^{h+1}
        for (int i = 0; i < n; i++) { pn[i] = p[i] - (1 << h); if (pn[i] < 0) pn[i] += n; }
        fill(cnt.begin(), cnt.begin() + cls, 0);
        for (int i = 0; i < n; i++) cnt[c[pn[i]]]++;
        for (int i = 1; i < cls; i++) cnt[i] += cnt[i - 1];
        for (int i = n - 1; i >= 0; i--) p[--cnt[c[pn[i]]]] = pn[i];
        cn[p[0]] = 0; cls = 1;
        for (int i = 1; i < n; i++) {
            pair<int, int> cur = {c[p[i]], c[(p[i] + (1 << h)) % n]}, prv = {c[p[i - 1]], c[(p[i - 1] + (1 << h)) % n]};
            if (cur != prv) cls++; cn[p[i]] = cls - 1;
        }
        swap(c, cn);
    }
    return vector<int>(p.begin() + 1, p.end());           // drop the sentinel
}
 
vector<int> sais(const vector<int>& s, int K) {             // s ends with a unique 0, values < K; returns SA including the sentinel
    int n = sz(s); if (n == 1) return {0};
    vector<char> S(n, 0); S[n - 1] = 1;                     // S-type flags
    for (int i = n - 2; i >= 0; i--) S[i] = s[i] < s[i + 1] || (s[i] == s[i + 1] && S[i + 1]);
    auto isLMS = [&](int i) { return i > 0 && S[i] && !S[i - 1]; };
    vector<int> cnt(K, 0), sa(n), bkt(K); for (int x : s) cnt[x]++;
    auto induce = [&](const vector<int>& lms) {
        fill(all(sa), -1);
        int sum = 0; for (int c = 0; c < K; c++) sum += cnt[c], bkt[c] = sum;          // bucket tails
        for (int i = sz(lms) - 1; i >= 0; i--) sa[--bkt[s[lms[i]]]] = lms[i];
        sum = 0; for (int c = 0; c < K; c++) bkt[c] = sum, sum += cnt[c];              // heads: L-type
        for (int i = 0; i < n; i++) { int j = sa[i] - 1; if (sa[i] > 0 && !S[j]) sa[bkt[s[j]]++] = j; }
        sum = 0; for (int c = 0; c < K; c++) sum += cnt[c], bkt[c] = sum;              // tails: S-type
        for (int i = n - 1; i >= 0; i--) { int j = sa[i] - 1; if (sa[i] > 0 && S[j]) sa[--bkt[s[j]]] = j; }
    };
    vector<int> lms; for (int i = 1; i < n; i++) if (isLMS(i)) lms.push_back(i);
    induce(lms);
    vector<int> name(n, -1); int cur = 0, prev = -1;        // name the LMS substrings in sorted order
    for (int i = 0; i < n; i++) {
        int p = sa[i]; if (!isLMS(p)) continue;
        bool diff = prev == -1;
        for (int d = 0; !diff; d++) {
            if (s[p + d] != s[prev + d] || S[p + d] != S[prev + d]) diff = true;
            else if (d > 0 && isLMS(p + d)) break;          // both substrings ended together: equal
        }
        if (diff) cur++;
        name[p] = cur - 1; prev = p;
    }
    vector<int> red; for (int i : lms) red.push_back(name[i]);
    vector<int> redSa;
    if (cur < sz(lms)) redSa = sais(red, cur);
    else { redSa.assign(sz(lms), 0); for (int i = 0; i < sz(lms); i++) redSa[red[i]] = i; }
    vector<int> sorted(sz(lms)); for (int i = 0; i < sz(lms); i++) sorted[i] = lms[redSa[i]];
    induce(sorted);
    return sa;
}
vector<int> suffixArraySAIS(const string& str) {
    vector<int> s(sz(str) + 1, 0); for (int i = 0; i < sz(str); i++) s[i] = (unsigned char)str[i] + 1;
    auto sa = sais(s, 257); return vector<int>(sa.begin() + 1, sa.end());
}
 
vector<int> kasai(const string& s, const vector<int>& sa) { // lcp[r] = LCP(sa[r-1], sa[r]), lcp[0] = 0
    int n = sz(s); vector<int> rk(n), lcp(n, 0);
    for (int i = 0; i < n; i++) rk[sa[i]] = i;
    for (int i = 0, k = 0; i < n; i++) {
        if (rk[i] == 0) { k = 0; continue; }
        int j = sa[rk[i] - 1];
        while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
        lcp[rk[i]] = k; if (k) k--;
    }
    return lcp;
}
 
struct SuffixStructure {                                    // SA + rank + LCP sparse table
    string s; vector<int> sa, rk, lcp; vector<vector<int>> sp;
    SuffixStructure(const string& str) : s(str), sa(suffixArray(str)), rk(sz(str)) {
        int n = sz(s); for (int i = 0; i < n; i++) rk[sa[i]] = i;
        lcp = kasai(s, sa); sp = {lcp};
        for (int k = 1; (1 << k) <= n; k++) { sp.emplace_back(n); for (int i = 0; i + (1 << k) <= n; i++) sp[k][i] = min(sp[k - 1][i], sp[k - 1][i + (1 << (k - 1))]); }
    }
    int lcpOf(int i, int j) const {                         // LCP of suffixes starting at i and j
        int n = sz(s); if (i == j) return n - i;
        int a = min(rk[i], rk[j]) + 1, b = max(rk[i], rk[j]) + 1, k = __lg(b - a);
        return min(sp[k][a], sp[k][b - (1 << k)]);
    }
    int compare(int l1, int r1, int l2, int r2) const {     // s[l1, r1) vs s[l2, r2): -1, 0, 1
        int len1 = r1 - l1, len2 = r2 - l2, k = (len1 && len2) ? min({lcpOf(l1, l2), len1, len2}) : 0;
        if (k == len1 || k == len2) return (len1 > len2) - (len1 < len2);
        return s[l1 + k] < s[l2 + k] ? -1 : 1;
    }
    int countOccurrences(const string& p) const {
        auto lo = partition_point(all(sa), [&](int i) { return s.compare(i, sz(p), p) < 0; });
        auto hi = partition_point(all(sa), [&](int i) { return s.compare(i, sz(p), p) <= 0; });
        return hi - lo;
    }
    ll distinctSubstrings() const { ll n = sz(s), t = n * (n + 1) / 2; for (int x : lcp) t -= x; return t; }
    string kthDistinct(ll k) const {                        // 1-indexed k-th distinct substring, "" if k too large
        for (int r = 0; r < sz(s); r++) { ll fresh = sz(s) - sa[r] - lcp[r]; if (k <= fresh) return s.substr(sa[r], lcp[r] + k); k -= fresh; }
        return "";
    }
};
 
int longestCommonSubstring(const vector<string>& strs) {   // over all k strings; O(n log n) via SA on ints
    int k = sz(strs); vector<int> s, owner;
    for (int i = 0; i < k; i++) { for (char c : strs[i]) s.push_back((unsigned char)c + k + 1), owner.push_back(i); s.push_back(i + 1); owner.push_back(-1); }
    s.push_back(0); owner.push_back(-1);                    // separators 1..k are distinct and smaller than letters
    int n = sz(s); vector<int> sa = sais(s, 256 + k + 2), rk(n), lcp(n, 0);
    for (int i = 0; i < n; i++) rk[sa[i]] = i;
    for (int i = 0, h = 0; i < n; i++) {                    // Kasai on ints; separators never match
        if (rk[i] == 0) { h = 0; continue; }
        int j = sa[rk[i] - 1];
        while (i + h < n && j + h < n && s[i + h] == s[j + h] && s[i + h] > k) h++;
        lcp[rk[i]] = h; if (h) h--;
    }
    if (k == 1) return sz(strs[0]);
    vector<int> have(k, 0); int distinct = 0, best = 0; deque<int> dq;   // window [l, r] of ranks, min lcp over (l, r]
    for (int l = 0, r = 0; r < n; r++) {
        int o = owner[sa[r]]; if (o >= 0 && have[o]++ == 0) distinct++;
        while (!dq.empty() && lcp[dq.back()] >= lcp[r]) dq.pop_back(); dq.push_back(r);
        while (distinct == k) {
            while (!dq.empty() && dq.front() <= l) dq.pop_front();
            if (!dq.empty() && l < r) best = max(best, lcp[dq.front()]);
            int ol = owner[sa[l]]; if (ol >= 0 && --have[ol] == 0) distinct--;
            l++;
        }
    }
    return best;
}

Karp–Miller–Rosenberg naming

The doubling table = class of gives deterministic equality of equal-length substrings ( iff the two overlapping halves of length agree), an exact alternative to hashing at memory; with binary search it gives LCE and is the basis of tandem-repeat and periodicity queries. DC3 / skew: sort the suffixes at positions recursively (names of character triples, a string of length ), sort the suffixes by (character, rank of the next suffix), and merge with comparisons because is a difference cover mod 3: . Difference covers mod also give sublinear-space LCE.

struct KMR {                                                // id[k][i] = class of s[i, i + 2^k)
    vector<vector<int>> id;
    KMR(const string& s) {
        int n = sz(s), LOG = 1; while ((1 << LOG) < n) LOG++;
        id.assign(LOG + 1, vector<int>(n));
        for (int i = 0; i < n; i++) id[0][i] = (unsigned char)s[i];
        for (int k = 0; k < LOG; k++) {
            int len = 1 << k; vector<pair<pair<int, int>, int>> a(n);
            for (int i = 0; i < n; i++) a[i] = {{id[k][i], i + len < n ? id[k][i + len] : -1}, i};
            sort(all(a)); int c = 0;
            for (int i = 0; i < n; i++) { if (i && a[i].first != a[i - 1].first) c++; id[k + 1][a[i].second] = c; }
        }
    }
    bool equal(int i, int j, int L) const {                 // s[i, i+L) == s[j, j+L), L >= 1
        int k = __lg(L);
        return id[k][i] == id[k][j] && id[k][i + L - (1 << k)] == id[k][j + L - (1 << k)];
    }
};

Distinct substrings of many ranges

For queries , count distinct non-empty substrings of , offline. Sweep : charge each distinct substring of to the start of its last occurrence; the answer is the sum of charges at starts (Fenwick tree with range add / range sum). Appending makes every suffix of end at : these are the strings on the suffix-link path of the prefix state in the suffix automaton. A link-cut-tree access recolours that path with ; each preferred path met had a single previous end and a contiguous length range , so starts lose their charge, and the new range gains it. Amortised pieces per access: .

struct DistinctSubstringsInRange {                          // lowercase s; queries [l, r)
    struct SAM {
        vector<array<int, 26>> next; vector<int> link, len; int last = 0;
        SAM() : next(1), link(1, -1), len(1, 0) { next[0].fill(-1); }
        int add(int c) {
            int cur = sz(next); next.push_back({}), next[cur].fill(-1), link.push_back(0), len.push_back(len[last] + 1);
            int p = last;
            while (p != -1 && next[p][c] == -1) next[p][c] = cur, p = link[p];
            if (p != -1) {
                int q = next[p][c];
                if (len[p] + 1 == len[q]) link[cur] = q;
                else {
                    int clone = sz(next); next.push_back(next[q]), link.push_back(link[q]), len.push_back(len[p] + 1);
                    while (p != -1 && next[p][c] == q) next[p][c] = clone, p = link[p];
                    link[q] = link[cur] = clone;
                }
            }
            return last = cur;
        }
    };
    vector<int> ch0, ch1, fa, col, tag, lo, mn, len; vector<ll> b1, b2; int n; vector<ll> answers;
    void addPrefix(vector<ll>& b, int i, ll v) { for (i++; i <= n; i += i & -i) b[i] += v; }
    void rangeAdd(int l, int r, ll v) { addPrefix(b1, l, v), addPrefix(b1, r + 1, -v); addPrefix(b2, l, v * l), addPrefix(b2, r + 1, -v * (r + 1)); }
    ll prefixSum(int i) { ll s1 = 0, s2 = 0; for (int j = i; j > 0; j -= j & -j) s1 += b1[j], s2 += b2[j]; return s1 * i - s2; }
    bool isRoot(int x) { return !fa[x] || (ch0[fa[x]] != x && ch1[fa[x]] != x); }
    void apply(int x, int c) { if (x) col[x] = tag[x] = c; }
    void push(int x) { if (tag[x] != -2) apply(ch0[x], tag[x]), apply(ch1[x], tag[x]), tag[x] = -2; }
    void pull(int x) { mn[x] = min({lo[x], mn[ch0[x]], mn[ch1[x]]}); }
    void rotate(int x) {
        int p = fa[x], g = fa[p]; bool right = ch1[p] == x;
        if (!isRoot(p)) (ch0[g] == p ? ch0[g] : ch1[g]) = x;
        fa[x] = g; int b = right ? ch0[x] : ch1[x];
        (right ? ch1[p] : ch0[p]) = b; if (b) fa[b] = p;
        (right ? ch0[x] : ch1[x]) = p; fa[p] = x; pull(p), pull(x);
    }
    void splay(int x) {
        vector<int> path = {x}; for (int y = x; !isRoot(y); y = fa[y]) path.push_back(fa[y]);
        for (int i = sz(path) - 1; i >= 0; i--) push(path[i]);
        while (!isRoot(x)) { int p = fa[x], g = fa[p]; if (!isRoot(p)) rotate((ch0[g] == p) == (ch0[p] == x) ? p : x); rotate(x); }
    }
    void access(int x, int r) {                             // recolour root..x with end position r
        for (int y = x, last = 0; y; last = y, y = fa[y]) {
            splay(y);
            if (col[y] >= 0) { int a = min(mn[ch0[y]], lo[y]), b = len[y], p = col[y]; rangeAdd(p - b + 1, p - a + 1, -1); }
            ch1[y] = last; pull(y);
        }
        splay(x); apply(x, r); rangeAdd(r - len[x] + 1, r, 1);
    }
    DistinctSubstringsInRange(const string& s, const vector<pair<int, int>>& qs) : n(sz(s)) {
        SAM sam; vector<int> prefixState(n);
        for (int i = 0; i < n; i++) prefixState[i] = sam.add(s[i] - 'a');
        int m = sz(sam.len);
        ch0.assign(m, 0), ch1.assign(m, 0), fa.assign(m, 0), col.assign(m, -1), tag.assign(m, -2);
        lo.assign(m, INT_MAX), mn.assign(m, INT_MAX), len = sam.len;
        for (int v = 1; v < m; v++) fa[v] = sam.link[v], lo[v] = mn[v] = sam.len[sam.link[v]] + 1;
        b1.assign(n + 2, 0), b2.assign(n + 2, 0);
        vector<vector<int>> byEnd(n + 1); for (int i = 0; i < sz(qs); i++) byEnd[qs[i].second].push_back(i);
        answers.assign(sz(qs), 0);
        for (int r = 0; r <= n; r++) {
            if (r > 0) access(prefixState[r - 1], r - 1);
            for (int i : byEnd[r]) answers[i] = prefixSum(n) - prefixSum(qs[i].first);
        }
    }
};