The sorted order of all suffixes of a string, stored as an array of starting positions. Paired with the LCP array, it answers most substring questions with far less memory than a suffix tree.

s = banana
sa  = [5, 3, 1, 0, 4, 2]      suffixes: a, ana, anana, banana, na, nana
lcp = [-, 1, 3, 0, 0, 2]      lcp[i] = LCP(sa[i-1], sa[i])

Construction — prefix doubling,

vector<int> suffixArray(string s) {
    s += '\x01';                                   // sentinel, smaller than everything
    int n = s.size();
    vector<int> sa(n), rank_(n), tmp(n);
    iota(sa.begin(), sa.end(), 0);
    for (int i = 0; i < n; i++) rank_[i] = s[i];
 
    for (int k = 1; k < n; k <<= 1) {
        auto cmp = [&](int a, int b) {
            if (rank_[a] != rank_[b]) return rank_[a] < rank_[b];
            int ra = a + k < n ? rank_[a + k] : -1;
            int rb = b + k < n ? rank_[b + k] : -1;
            return ra < rb;
        };
        sort(sa.begin(), sa.end(), cmp);
        tmp[sa[0]] = 0;
        for (int i = 1; i < n; i++) tmp[sa[i]] = tmp[sa[i-1]] + cmp(sa[i-1], sa[i]);
        rank_ = tmp;
    }
    return vector<int>(sa.begin() + 1, sa.end());   // drop the sentinel
}

Replacing sort with a two-pass radix sort gives . SA-IS and DC3 give but are much longer.

The doubling idea is exactly the KMR algorithm.

LCP array — Kasai,

vector<int> kasai(const string& s, const vector<int>& sa) {
    int n = s.size();
    vector<int> rank_(n), lcp(n, 0);
    for (int i = 0; i < n; i++) rank_[sa[i]] = i;
    for (int i = 0, k = 0; i < n; i++) {
        if (rank_[i] == 0) { k = 0; continue; }
        int j = sa[rank_[i] - 1];
        while (i + k < n && j + k < n && s[i+k] == s[j+k]) k++;
        lcp[rank_[i]] = k;
        if (k) k--;
    }
    return lcp;
}

What SA + LCP answers

QueryMethodCost
Does pattern occur?binary search the SA
Number of occurrences of two binary searches
Number of distinct substrings
Longest repeated substring
Longest common substring of two stringsconcatenate with a separator; max lcp across the boundary
LCP of any two suffixesRMQ over the LCP array with a sparse table
-th lexicographic substringwalk the SA with lcp
Compare two substringsLCP query + one character
Longest common substring of stringsconcatenate with distinct separators; sliding window over the SA

The LCP interval structure

The LCP array implicitly encodes a suffix tree: an internal node of the suffix tree corresponds to a maximal interval of the SA whose minimum LCP equals that node’s string depth. Building this “LCP interval tree” (with a monotonic stack) lets you run suffix-tree algorithms without a suffix tree.

This is why “suffix array + LCP” is usually the right answer: it has the power of a suffix tree at a fraction of the memory and the implementation effort.

Construction methods

MethodTimeLinesWhen
Sort suffixes naively3
Prefix doubling + sort~20
Prefix doubling + radix~35
DC3~60
SA-IS~90the fastest in practice

Write the version from memory in a contest; keep SA-IS in a template file.

Suffix array vs the alternatives

Suffix arraySuffix automatonSuffix tree
Memory ints — smallest
Build easy, hard, 30 lines, very hard
Online (append characters)✔ (Ukkonen)
Lexicographic order✔ naturalneeds work
Count distinct substrings after LCP directly
Contest defaultwhen order mattersusually thisrarely

See also: Kasai · Suffix Automaton · SA-IS