Some conventions
For definitions related to strings, see String Basics.
String indices start at .
The length of string is .
"Suffix " refers to the suffix beginning with the -th character. When stored, is used to represent the suffix of string .
What is a suffix array?
A suffix array mainly involves two arrays: and .
denotes the index of the -th smallest suffix after sorting all suffixes; this is the suffix array, also called the index array below;
denotes the rank of suffix . It is an important auxiliary array, also called the rank array below.
These two arrays satisfy the property .
Explanation
An example of a suffix array:

How do we compute a suffix array?
The O(n^2logn) approach
This approach is probably easy to come up with: sort an array containing all suffix strings using sort. Since the sorting performs string comparisons and each string comparison requires character comparisons, the time complexity of this sorting is .
The O(nlog^2n) approach
This approach uses the idea of doubling.
First, sort all substrings of length in string , that is, each character, obtaining the sorted index array and rank array .
The doubling process:
-
Use the ranks of two substrings of length , namely and , as the first and second sorting keys to sort every substring of length in string : , obtaining and ;
-
Then use the ranks of two substrings of length , namely and , as the first and second sorting keys to sort every substring of length in string : , obtaining and ;
-
Continue doubling in this way. Use the ranks of substrings of length , namely and , as the first and second sorting keys to sort every substring of length in string , , obtaining and . As with lexicographic sorting, when , treat as negative infinity;
-
is the rank of substring . Thus, when , the resulting index array is the suffix array we need.
Process
Illustration of the doubling sort:

Clearly, the doubling process takes iterations, and sorting the substrings with sort in each iteration takes ; each substring comparison costs character comparisons.
In addition, after each sort, updating takes an extra time, but this is negligible compared with .
Therefore, the time complexity of this algorithm is .
Implementation
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
constexpr int N = 1000010;
char s[N];
int n, w, sa[N], rk[N << 1], oldrk[N << 1];
// Allocate an array twice as large to prevent out-of-bounds access when accessing rk[i+w].
// Of course, one can also check for out-of-bounds access before accessing it, but simply using an array twice as large is more convenient.
int main() {
int i, p;
scanf("%s", s + 1);
n = strlen(s + 1);
for (i = 1; i <= n; ++i) sa[i] = i, rk[i] = s[i];
for (w = 1; w < n; w <<= 1) {
sort(sa + 1, sa + n + 1, [](int x, int y) {
return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
}); // A lambda is used here
memcpy(oldrk, rk, sizeof(rk));
// Since the original rk will be overwritten while computing rk, copy it first.
// If two substrings are equal, their corresponding rk values must also be equal, so duplicates must be removed.
for (p = 0, i = 1; i <= n; ++i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++p;
}
}
}
for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
The O(nlogn) approach
In the approach just described, each sort takes . If we can sort in , we can compute the suffix array in .
Prerequisites: Counting Sort, Radix Sort.
During suffix-array computation, the sorting keys are ranks, whose value range is , and the sort has two keys. Thus, radix sort can optimize it to .
Implementation
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
constexpr int N = 1000010;
char s[N];
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], cnt[N];
int main() {
int i, m, p, w;
scanf("%s", s + 1);
n = strlen(s + 1);
m = 127;
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
memcpy(oldrk + 1, rk + 1, n * sizeof(int));
for (p = 0, i = 1; i <= n; ++i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]]) {
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++p;
}
}
for (w = 1; w < n; w <<= 1, m = n) {
// Counting-sort the second key: id[i] + w
memset(cnt, 0, sizeof(cnt));
memcpy(id + 1, sa + 1,
n * sizeof(int)); // id stores a copy of sa, essentially serving as oldsa
for (i = 1; i <= n; ++i) ++cnt[rk[id[i] + w]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[id[i] + w]]--] = id[i];
// Counting-sort the first key: id[i]
memset(cnt, 0, sizeof(cnt));
memcpy(id + 1, sa + 1, n * sizeof(int));
for (i = 1; i <= n; ++i) ++cnt[rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[id[i]]]--] = id[i];
memcpy(oldrk + 1, rk + 1, n * sizeof(int));
for (p = 0, i = 1; i <= n; ++i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++p;
}
}
}
for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
Some constant-factor optimizations
If you submit the code above to LOJ #111: Suffix Sorting:

This is because the constants of the code above are indeed quite large.
The second key does not need counting sort
Consider the essence of sorting by the second key. It is simply to put the whose range exceeds the string (that is, ) at the head of the array, then put the rest in their original order:
int cur = 0;
for (int i = n - w + 1; i <= n; i++) id[++cur] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > w) id[++cur] = sa[i] - w;
Optimize the value range of counting sort
After updating each time, we calculate a . This is exactly the value range of , so we can use it as the value range.
If all ranks are distinct, the suffix array can be generated directly
Consider the new array. If its value range is , then every rank is different, so no further sorting is needed.
Implementation
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
constexpr int N = 1000010;
char s[N];
int n;
int m, p, rk[N * 2], oldrk[N], sa[N * 2], id[N], cnt[N];
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
m = 128;
for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
for (int w = 1;; w <<= 1, m = p) { // m = p is the value-range optimization
int cur = 0;
for (int i = n - w + 1; i <= n; i++) id[++cur] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > w) id[++cur] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; i++) cnt[rk[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[id[i]]]--] = id[i];
p = 0;
memcpy(oldrk, rk, sizeof(oldrk));
for (int i = 1; i <= n; i++) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w])
rk[sa[i]] = p;
else
rk[sa[i]] = ++p;
}
if (p == n) break; // No further sorting is needed when p = n
}
for (int i = 1; i <= n; i++) printf("%d ", sa[i]);
return 0;
}
The O(n) approach
For general problems, the doubling method for suffix arrays, with its small constants, is entirely sufficient. The parts other than suffix-array computation also often have complexity, so doubling will not become the bottleneck.
However, for special problems, problems with tight time limits, or when you want to pursue shorter runtimes, you need to learn an method for computing suffix arrays.
SA-IS
See Induced Sorting and the SA-IS Algorithm; its comments page is also useful.
DC3
See [2009] Suffix Arrays—A Powerful Tool for Processing Strings by Luo Suiqian.
Applications of suffix arrays
Finding the position of the smallest cyclic shift
Duplicating string to form turns this into a suffix-sorting problem.
Example problem: 「JSOI2007」Character Encryption.
Finding a substring in a string
The task is to find a pattern string online in a text string . Online means that we know the text string in advance, but learn the pattern string only when a query is made. We can first construct the suffix array of , then search for substring . If substring occurs in , it must be a prefix of some suffixes of . Since all suffixes have already been sorted, we can find by binary-searching in the array. Comparing substring with the current suffix takes time, so finding the substring takes time. Note that if the substring occurs multiple times in , all occurrences are adjacent in the array. Thus, the number of occurrences can be found with another binary search, and outputting every occurrence position is also easy.
Minimizing lexicographic order by taking characters from either end
Example problem: 「USACO07DEC」Best Cow Line.
Problem statement: Given a string, take one character from either the beginning or the end at each step to form a string. Find the lexicographically smallest string that can be formed.
Solution
The brute-force approach checks at each step, in the worst case in time, whether to take the first or last character (that is, comparing the string obtained by taking the first character with the reverse of the string obtained by taking the last character). It is enough to optimize this decision process.
Since we need to compare suffixes of the original string with suffixes of its reverse, concatenate the reverse to the original string and insert a character not appearing in either string in the middle (such as #; the code can directly use the null character). Computing the suffix array then lets us make this comparison in time.
Reference code
#include <cctype>
#include <cstring>
#include <iostream>
using namespace std;
constexpr int N = 1000010;
char s[N];
int n, sa[N], id[N], oldrk[N * 2], rk[N * 2], px[N], cnt[N];
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, w, m = 200, p, l = 1, r, tot = 0;
cin >> n;
r = n;
for (i = 1; i <= n; ++i)
while (cin >> s[i], !isalpha(s[i]));
for (i = 1; i <= n; ++i)
rk[i] = rk[2 * n + 2 - i] = s[i]; // Concatenate the original and reversed strings, leaving one character unused in the middle
n = 2 * n + 1;
// Compute the suffix array
for (i = 1; i <= n; ++i) ++cnt[rk[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w *= 2, m = p) { // m=p is the optimization of the counting-sort value range
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
// Use the suffix array to make the comparison in O(1) time
while (l <= r) {
cout << (rk[l] < rk[n + 1 - r] ? s[l++] : s[r--]);
if ((++tot) % 80 == 0) cout << '\n'; // Newline
}
return 0;
}
The height array
LCP (longest common prefix)
The LCP of two strings and is the largest () such that .
Below, denotes the longest common prefix (length) of suffixes and .
Definition of the height array
, that is, the longest common prefix of the suffix ranked and the suffix ranked immediately before it.
can be regarded as .
O(n) A lemma needed to compute the height array
Proof
When , the inequality is obvious (the right-hand side is at most ).
When :
By the definition of , .
Since suffix and suffix have a longest common prefix of length ,
let us denote this longest common prefix by . (Here is a character, and is a nonempty string of length .)
Then suffix can be written as , and suffix can be written as . (; may be an empty string, while is nonempty.)
Furthermore, suffix can be written as , and suffix exists and is .
Since suffix is only one position below suffix , which is suffix , in rank order, and ,
we have suffix . Clearly, suffix and suffix share the prefix .
Therefore, is at least , which gives .
O(n) Implementation of the height array computation
The lemma above lets us compute it by brute force:
for (i = 1, k = 0; i <= n; ++i) {
if (rk[i] == 0) continue;
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
height[rk[i]] = k;
}
never exceeds and is decremented at most times, so it is incremented at most times in total. The overall complexity is .
Applications of the height array
Longest common prefix of two substrings
Intuitively, if is always greater than some value, those many initial characters have never changed; conversely, since the suffixes are already sorted, they cannot change and then change back.
For a rigorous proof, see [2004] Suffix Array by Xu Zhilei.
With this theorem, finding the longest common prefix of two substrings becomes an RMQ problem.
Comparing the order of two substrings of a string
Suppose we need to compare and .
If , $A [!note]- Solution
A string occurring at least times means that, after suffix sorting, at least consecutive suffixes have this substring as a common prefix.
Therefore, find the minimum of every consecutive values of , then the maximum of these minima is the answer.
A monotonic queue can solve this in , but other methods are also sufficient to get AC.
Reference code
#include <cstring>
#include <iostream>
#include <set>
using namespace std;
constexpr int N = 40010;
int n, k, a[N], sa[N], rk[N], oldrk[N], id[N], px[N], cnt[1000010], ht[N], ans;
multiset<int> t; // multiset is the easiest implementation to write
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int i, j, w, p, m = 1000000;
cin >> n >> k;
--k;
for (i = 1; i <= n; ++i) cin >> a[i]; // Compute the suffix array
for (i = 1; i <= n; ++i) ++cnt[rk[i] = a[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w <<= 1, m = p) {
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
for (i = 1, j = 0; i <= n; ++i) { // Compute height
if (j) --j;
while (a[i + j] == a[sa[rk[i] - 1] + j]) ++j;
ht[rk[i]] = j;
}
for (i = 1; i <= n; ++i) { // Find the maximum of all minima
t.insert(ht[i]);
if (i > k) t.erase(t.find(ht[i - k]));
ans = max(ans, *t.begin());
}
cout << ans;
return 0;
}
Whether some string occurs at least twice without overlapping in the text
We can binary-search the length of the target string, divide the array into several consecutive segments whose LCP is at least , and use RMQ on each segment to find the maximum and minimum indices among the occurrences. If the distance between these two indices meets the condition, then there must be a string of length that occurs twice without overlapping.
Several consecutive equal substrings
We can enumerate the length of the repeated string, divide the entire string into blocks of length , and query the LCP and LCS of the beginnings of adjacent blocks. See [2009] Suffix Arrays—A Powerful Tool for Processing Strings for details.
Example problem: 「NOI2016」Excellent Split.
Combining with a disjoint-set union
Some problems require dividing the suffix array into several consecutive segments whose LCP length is at least a given value; equivalently, divide the array into consecutive segments whose minimum is at least a given value and compute the answer for each segment. If there are multiple queries, we can process them offline. Observe that as the given value decreases monotonically, the number of intervals satisfying the condition keeps decreasing, while each new interval is formed by joining two or more original intervals, and the parts of a new interval that were not contained in an original interval all have value equal to the newly decreased value. We only need to maintain a disjoint-set union, merging adjacent intervals each time and maintaining the aggregate information.
Classic problem: 「NOI2015」Wine Tasting Conference
Combining with a segment tree
Some problems ask for the first several numbers satisfying a condition, and these numbers all lie within an interval in suffix order. We can then use the merge-sort property to merge information from two nodes, and use a segment tree to maintain and query the answer for an interval.
Combining with a monotonic stack
Example problem: 「AHOI2013」Difference
Solution
The first two terms of the addends are easy to handle: (each suffix appears times, and the total suffix length is ). The key is the last term, namely the pairwise LCP of suffixes.
We know that is equivalent to . Therefore, we can regard as the contribution to the answer of .
Consider which suffix LCPs each position contributes to. It is equivalent to choosing one suffix from the consecutive suffixes to its left whose values are greater than it, and one suffix from the consecutive suffixes to its right whose values are no less than it. This can be computed with a monotonic stack.
The monotonic-stack part is similar to Luogu P2659 Beautiful Sequence and the horizontal scanning method.
Reference code
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
constexpr int N = 500010;
string s;
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], px[N], cnt[N], ht[N], sta[N],
top, l[N];
long long ans;
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
int main() {
int i, k, w, p, m = 300;
cin >> s;
n = s.size();
s = " " + s;
ans = 1ll * n * (n - 1) * (n + 1) / 2;
// Compute the suffix array
for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
for (w = 1; w < n; w <<= 1, m = p) {
for (p = 0, i = n; i > n - w; --i) id[++p] = i;
for (i = 1; i <= n; ++i)
if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt, 0, sizeof(cnt));
for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof(rk));
for (p = 0, i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
}
// Compute height
for (i = 1, k = 0; i <= n; ++i) {
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
ht[rk[i]] = k;
}
// Maintain the monotonic stack
for (i = 1; i <= n; ++i) {
while (ht[sta[top]] > ht[i]) --top; // top is similar to a pointer
l[i] = i - sta[top];
sta[++top] = i;
}
// Finally, use the monotonic stack to calculate ans
sta[++top] = n + 1;
ht[n + 1] = -1;
for (i = n; i >= 1; --i) {
while (ht[sta[top]] >= ht[i]) --top;
ans -= 2ll * ht[i] * l[i] * (sta[top] - i);
sta[++top] = i;
}
cout << ans;
return 0;
}
A similar problem: 「HAOI2016」Finding Equal Characters.
Exercises
- UVa 760 - DNA Sequencing
- UVa 1223 - Editor
- Codechef - Tandem
- Codechef - Substrings and Repetitions
- Codechef - Entangled Strings
- Codeforces - Martian Strings
- Codeforces - Little Elephant and Strings
- SPOJ - Ada and Terramorphing
- SPOJ - Ada and Substring
- UVa - 1227 - The longest constant gene
- SPOJ - Longest Common Substring
- UVa 11512 - GATTACA
- QOJ 11240 - Suffixes and Palindromes
- GYM - Por Costel and the Censorship Committee
- UVa 1254 - Top 10
- UVa 12191 - File Recover
- UVa 12206 - Stammering Aliens
- Codechef - Jarvis and LCP
- Luogu P8617 - Repeated Patterns
- UVa 11107 - Life Forms
- UVa 12974 - Exquisite Strings
- UVa 10526 - Intellectual Property
- UVa 12338 - Anti-Rhyme Pairs
- DevSkills Reconstructing Blue Print of Life
- UVa 12191 - File Recover
- SPOJ - Suffix Array
- Gym 102470J - Stammering Aliens
- SPOJ - LCS2
- Codeforces - Fake News (hard)
- SPOJ - Longest Commong Substring
- SPOJ - Lexicographical Substring Search
- Codeforces - Forbidden Indices
- Codeforces - Tricky and Clever Password
- Gym 101470B - Circle of digits
References
This page (the part introduced by 4070a9b) is mainly translated from the blog post Суффиксный массив and its English translation, Suffix Array. The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.
Papers: