Introduction
While the usual
In this blog, we'll learn how to build suffix arrays in linear time, using the algorithm I learned for this purpose. It's called SA-IS ("Suffix Array by Induced Sorting", by Nong, Zhang, and Chan), and was originally described in this paper.
1. Preliminaries
We have a string
For the sake of convenience:
- We assume that
for all . - We add a unique sentinel element with value
at the end of . Note that this sentinel is the unique minimum in and the length now becomes . - We define
to be the -th suffix, i.e. . At several places ahead, I will simply use " -th suffix" or "suffix " to refer to .
The goal is to construct the suffix array of
2. L and S-type suffixes
The key insight that SAIS is built around is that there's a lot of information hidden in comparisons of adjacent suffixes of a string (i.e.
As such, we define two types of suffixes:
- Suffix
is said to be of L-type, if and . - Suffix
is said to be of S-type, if or .
We will now make a series of observations about these two types in quick succession, so buckle up.
First, we characterise the structure of both types.
Theorem 2.1
Suffix
is L-type iff and for the smallest such that .
Simply put, all L-type suffixes have the form [>= 1 occurrences of symbol b][1 occurrence of a]... where
Proof
Let
Now, the other direction - if [x >= 1 occurrences of b] [1 occurrence of a] where
Theorem 2.2
Suffix
is S-type iff or for the smallest such that .
Simply put, all S-type suffixes, save for the sentinel, have the form [>= 1 occurrences of symbol b][1 occurrence of c]... where
The proof is symmetrical to 2.1.
Theorem 2.3
All the positions in
can be classified as L or S-type in .
Denote the type of the
S-type.- For
:- If
, = S-type. - If
, = L-type. - Else,
.
- If
Proof
We simply use 2.1 and 2.2 for all
- If
, then suffix is of the formb c ...where and therefore must be S-type. - If
, then suffix is of the formb a ...where and therefore must be L-type. - If
, then suffix is either of the form[>= 1 occurrences of b][1 occurrence of a]...or[>= 1 occurrences of b][1 occurrence of c]..., and this determines its type. If we prepend onebto either form ( ), then the form doesn't change and neither does the type.
Theorem 2.4
If
, suffix is of L-type, and suffix is of S-type, then .
Proof
[>= 1 occurrences of b] [1 occurrence of a]... and [>= 1 occurrences of b][1 occurrence of c]....
It's easy to see that at the smallest
In every case, we have
Now, theorem 2.4 is quite powerful as it allows us to characterise the structure of the suffix array in a very friendly "bucketed" manner. We can observe that the suffix array must take the following form:
- First, we have L-type suffixes that start with symbol
. - Then, we have S-type suffixes that start with symbol
. - Then, L-type suffixes that start with symbol
. - Then, S-type suffixes that start with symbol
. - Then, L-type suffixes that start with symbol
. - Then, S-type suffixes that start with symbol
. - ...and so on
In other words:
- The suffix array consists of several disjoint buckets, with each bucket corresponding to suffixes starting with a certain symbol.
- The buckets are ordered by the starting symbol.
- Within a particular bucket, we first have all the L-type suffixes, and then all the S-type suffixes.
It's easy to see that these bucket boundaries can also be computed in
Code
// Given n and T
int n;
vector<int> T;
vector<int> cnt(n + 1, 0);
for(auto x : T)
cnt[x]++;
auto tail = cnt;
for(int i = 1; i <= n; i++)
tail[i] += tail[i - 1];
auto head = tail;
for(int i = 0; i <= n; i ++)
head[i] -= cnt[i];
//now, bucket for element i is [head[i], tail[i])
3. LMS-types
We define a suffix
- Suffix
is of S-type. - Suffix
is of L-type.
Let
A key observation is that there are
Theorem 3.1
If
is LMS-type, then .
Proof
If
Now, let's introduce a very helpful visualisation for strings that I'll just call a "slope view". The slope view of any given string will be obtained by us drawing downward or upward slants between two adjacent suffixes based on the result of their comparison (i.e. the type of the left suffix).
Let's look at some visualisations for baacbcbacbccdaa$ (where $ is the appended unique minimum). I know I said that
A few observations:
- LMS-types correspond to valleys in the slope view.
- An upward slope (from left to right) corresponds to S-type suffixes.
- A downward slope corresponds to L-type suffixes.
- The second-last character is always L-type, as the last character is the unique minimum. Since the last position is definitionally S-type, this also implies that it's always LMS-type.
Also recall theorem 2.4. The buckets in the SA for
4. Induced sorting
Now, after having tunnel-visioned on the classification of these L/S types for so long, let's return to our original goal of finding
4.1 L-induce
Say we were tasked with producing
The observation that the entire algorithm is built upon is that we don't need any S-type suffixes except the LMS-types to induce the sorted order of L-type suffixes.
Theorem 4.1
Given the sorted order of LMS-suffixes (a permutation
of where for ), we can compute in time all where is an L-type position. Less formally, we can fill out all the "L-buckets" of in time.
The rest of subsection 4.1 will be spent realising an algorithm which does this.
Let's first consider a really inefficient procedure to sort L-type suffixes while only making use of LMS-types (besides the L-types). This is really the sort of algorithm where it's simpler to use an unambiguous, real programming language rather than pseudocode, so you'll have to bear with C++ snippets throughout.
Procedure 1
// Given n, T, Q, S, bucket boundaries (see code in theorem 2.4)
int n;
vector<int> T, Q, head, tail;
vector<vector<int>> S; //suffixes
vector<int> A(n + 1, -1);
set<pair<vector<int>, int>> q;
auto extend = [&](int i) -> void
{
int j = i - 1;
if(j >= 0 and is_L_type(Q[j]))
q.insert({S[j], j});
};
for(int i = 0; i <= n; i ++)
if(is_LMS_type(Q[i]))
q.insert({S[i], i});
while(!q.empty())
{
auto [v, i] = *q.begin();
q.erase(q.begin());
//we do not place the LMS types into A
if(is_L_type(Q[i]))
{
A[head[T[i]]] = i;
head[T[i]] ++;
}
extend(i);
}
This inefficient procedure essentially does the following:
- It first inserts all the LMS-suffixes into a heap (that uses the standard lexicographical comparator).
- Then, it repeatedly picks the smallest suffix from the heap and:
- If the suffix corresponds to an L-type position
, it places at the first free position in the L-bucket corresponding to in . - Then, if position
is L-type, it inserts into the heap (i.e. it extends the removed suffix by one character on the left, or equivalently in the slope-view - moves one step up to the left).
- If the suffix corresponds to an L-type position
Before we prove that this procedure is correct, I encourage you to intuit why this is correct by visualising the slope view. We essentially "seed" the heap by inserting all the valley positions, and then inch up each L-type slope to the left (remember, L-type slopes slant upwards from right to left).
Anyway, why is the procedure correct?
Notice that when suffix
Now consider the manner in which we insert elements into
What about coverage? Does every L-type position eventually get placed into
Now, we make a subtle modification to this procedure that preserves correctness, but allows us to later optimise it. Consider the following:
Procedure 2
// Given n, T, Q, S, bucket boundaries (see code in theorem 2.4)
int n;
vector<int> T, Q, head, tail;
vector<vector<int>> S; //suffixes
vector<int> A(n + 1, -1); //suffix array
set<pair<vector<int>, int>> q;
auto extend2 = [&](int i) -> void
{
int j = i - 1;
if(j >= 0 and is_L_type(Q[j]))
{
A[head[T[j]]] = j;
head[T[j]] ++;
q.insert({S[j], j});
}
};
for(int i = 0; i <= n; i ++)
if(is_LMS_type(Q[i]))
q.insert({S[i], i});
while(!q.empty())
{
auto [v, i] = *q.begin();
q.erase(q.begin());
extend2(i);
}
What changed? Instead of placing L-type
Why is this procedure correct? First, observe that elements are still deleted from the heap in non-decreasing order.
All L-type positions are necessarily placed within their buckets in
Consider any two L-type positions
This proves that the L-regions of
Notice that our proof only relied on suffixes being deleted from the heap in non-decreasing order.
Theorem 4.2
Sequentially calling
extend2(i)on some non-decreasing sequence of suffixeswhich contains all L/LMS-type positions exactly once results in the L-regions of being correctly filled.
Now, how can we do this faster?
First, we divorce extend2(i) from the heap. From this point onward, it will no longer add anything to the heap.
Let's make use of Theorem 4.2. Imagine that we had access to some pre-built, valid extend2(A'[i]) in increasing order of extend2(A'[i]) only possibly places an element into
What if we could do the following at the same time?:
- Build
. - Use
itself as a pre-built that we walk from left to right and callextend2(A'[i])on?
Effectively (and I switch to pseudo-code now):
A = [-1] * (n + 1)
for i from 0 to n:
if (A[i] != -1) and (A[i] is L or LMS type):
extend2(A[i])
This is of course incorrect, for the simple reason that everything remains -1, but the fix is smaller than you might expect.
The problem is illustrated well by considering this procedure's equivalence to procedure 2. Remember that we delete suffixes
- First, we delete all indices in the L-bucket of
(L-suffixes that begin with ). - Then, LMS indices in the S-bucket of
. - Then, all indices in the L-bucket of
. - Then, LMS indices in the S-bucket of
- and so on
The issue here is that while we don't need the heap at all to handle the L-regions, it's doing critical work for us in the S-regions.
In particular, we can already modify procedure 2 to run in linear time for the L-regions in the following manner:
Procedure 2.5
A = [-1] * (n + 1)
put {S[i], i} for all LMS-type i into heap H
# we iterate over the first character of suffixes
for character c from 0 to n:
# we first process the L-region of c
let A[l, r] be the L-region for c
for i from l to r:
extend2(A[i])
# we then process the LMS-types of c, naively
while H is not empty and the smallest suffix in H begins with c:
pop out {S[i], i}, the smallest element of H
extend2(i)
Why is this correct? Because extend2(i) is called on exactly the same sequence of
Annoying Proof
We can prove this using induction. Assume that the first extend2() are made on the exact same sequence of indices in both procedures. Then let the
- If
is LMS type: Suffix is then present within both heaps from the very beginning. We will make use of the fact that no two suffixes can be equal. Procedure 2 would have already deleted all strings strictly smaller than it from the heap (remember that it callsextend2()in strictly increasing order of suffixes), and since 2.5 also calledextend2()for the same indices, it would also have deleted all suffixes smaller than from its own heap. Therefore, at this point of time must be the smallest string in procedure 2.5's heap, and we will pop it out now. - If
is L-type: Then it will be placed somewhere in the L-region of . Notice that its position in both procedures is determined solely by the number of times we callextend2(j)for such that before we callextend2(i + 1). All of these calls lie within the first calls ( is L-type so ), and are therefore the same for both procedures, implying that gets placed at the same position in both.
Looking at the second stage for each
Let's finally look at the L-induce procedure, which takes
Procedure 3
// Given n, T, Q, and the bucket boundaries (see code in theorem 2.4)
int n;
vector<int> T, Q, head, tail;
// Of course, given G too!
vector<int> G;
vector<int> A(n + 1, -1); //suffix array
// Seeding - we put LMS positions at the tails of their S-buckets
reverse(G.begin(), G.end());
for(auto i : G)
A[--tail[T[i]]] = i;
auto extend2 = [&](int i) -> void
{
int j = i - 1;
if(j >= 0 and is_L_type(Q[j]))
{
A[head[T[j]]] = j;
head[T[j]] ++;
}
};
// induce L positions into A
auto l_induce = [&]() -> void
{
for(int i = 0; i <= n; i ++)
if(A[i] != -1)
extend2(A[i]);
};
l_induce();
// clean up LMS types from A
for(int i = 0; i <= n; i ++)
if(A[i] != -1 and is_LMS_type(Q[A[i]]))
A[i] = -1;
It's easy to see that this procedure is correct (it's just the previous procedure with the second stage for each
4.2 S-induce
Let us now deal with the S regions.
The basic idea is symmetrical - just like we processed L-suffixes in non-decreasing order to fill the L-regions (left to right in
Anyway, let's look at the procedure and then talk about it:
Procedure 4
// Given n, T, Q, bucket boundaries, and A with L-regions computed
// Note that these are freshly computed bucket boundaries, not those
// modified in the L phase.
int n;
vector<int> T, Q, head, tail, A;
auto extend3 = [&](int i) -> void
{
int j = i - 1;
if(j >= 0 and is_S_type(Q[j]))
{
tail[T[j]] --;
A[tail[T[j]]] = j;
}
};
// place the sentinel position separately
A[0] = n;
// induce S positions into A
auto s_induce = [&]() -> void
{
for(int i = n; i >= 0; i --)
if(A[i] != -1)
extend3(A[i]);
};
s_induce();
Why does this work? The proof is structurally symmetric to that of L-induce (we now process strings in non-increasing order, so we go from right to left in extend3(A[i]) can only add something to
Thankfully:
is the unique minimum and must therefore be the very first element in , so we manually place it.- Because it's the unique minimum,
is L-type. Therefore, all other S-type positions have some peak after them (a "Leftmost L type").
I leave the formal proof of correctness to the reader, and return to the broader picture.
We now know that just having
5. Sorting LMS-suffixes
Okay, so how do we sort LMS suffixes efficiently? The S induce would of course give us their sorted order, but that needs the L induce, which needs the sorted order... It's a bit of a chicken-and-egg problem.
Let's think outside the box. The observant reader will remember that the number of LMS positions,
Let's imagine a magical procedure compress(T) which gives us such a SA(T), which computes the SA of
#input T satisfies properties from section 1
SA(T):
N = |T|
#base case
if N == 1:
return [0]
T2 = compress(T)
G = SA(T2)
for i from 0 to |G| - 1:
G[i] = l[G[i]] #G[i]-th LMS position in T
A = [-1] * (N)
run L induce on A using G
run S induce
return A
What would the time complexity of SA(T),
All that remains is to design such a compress(T).
Since the compress(T) is going to correspond to the
5.1 LMS Substrings
We define the LMS-end of position
Now, return to compress(T). We wish to find a labelling function
is an integer in- The last element of
is the unique minimum. - The
-th suffix of is smaller than the -th, iff .
To recap, why do we need these conditions? The first and second are needed so that compress(T) can directly be passed to SA(T) (which assumes that the conditions from section 1 are met). The third is needed so that
The first and second conditions are relatively simple to satisfy, but the third isn't. A condition on
Consider the labelling function:
Let's compare the
- Since the last character of
is the unique minimum, no suffix of can be a proper prefix of another suffix. - Therefore, consider the smallest
such that . Clearly . - Let
. Since , we have (easy to prove, left as an exercise). Now, . - We know that
( ). There are two cases:- If neither
nor is a proper prefix of the other, then ! Therefore, in this case, we have . - Otherwise, WLOG assume
is a proper prefix of . In this case, the lex comparator says . For to now be satisfied, we need . Let's examine this in more detail.
- If neither
Surprisingly, properties of the L/S classification trivialise this. Let LMS S S S S ... L L L L LMS. Since
Recall theorem 2.4! Since
We indeed can! We introduce the alex comparator,
- If
under lex, then they're equal under alex too. - Otherwise if neither
nor is a proper prefix of the other, then . - Otherwise,
.
Simply put, it's just the standard lexicographical comparator, except we flip the comparison result when one string is a proper prefix of the other.
Also, we can show that alex imposes a total order. Why? Because
Redefine
is an integer in - trivially satisfied.- The last element of
is the unique minimum - satisfied because remains the unique minimum amongst all full LMS substrings, so . - The
-th suffix of is smaller than the -th, iff .
Consider the third condition. When we previously attempted to prove
- For character
, and strings and , , and . - For full LMS substrings
and :- If neither was a prefix of the other,
. .- When
is a proper prefix of , we required , but our old yielded .
- If neither was a prefix of the other,
Clearly, our new definition of
5.2 Sorting LMS Substrings under alex
Okay, so we've finally found a suitable definition for
What if we just sort full LMS substrings in alex order, and then label them? Namely, compute an array
N = |T|
somehow compute X
R = [-1] * N
label = -1, last = -1
for i in X:
if last == -1 or !equal(P[last], P[i])
label += 1
R[i] = label
last = i
One concern is that assuming equal(A, B) function is implemented naively, i.e. in
Alright, so we just need to sort full LMS substrings under alex now. It shouldn't be surprising that we'll lean on the machinery (L and S induce) we built for sorting suffixes previously. Further, we'll sort all LMS substrings, and then just extract the subsequence of full LMS substrings from this sorted order.
Let's state our goal more formally. We want to compute array
Now, consider the L-induce, which was previously used to sort L-type suffixes. Can we repurpose it to sort L-type LMS-substrings instead? When we previously discussed it, we seeded
But now that we're sorting
The answer is yes, but we'll need to first prove some facts about LMS substrings.
Theorem 5.1
For any LMS-substring
with , its second-to-last character is strictly greater than the last.
This is just theorem 3.1 again, but it's quite fundamental to our arguments ahead, so I restate it.
Theorem 5.2
If position
is L-type, then .
Proof
Since position
- If
is LMS type, then - Otherwise,
.- If
, then trivially. - Otherwise, we know that for the smallest
where , we have (definition of L-type suffixes). Since , we must have , and the two strings differ before either ends.
- If
Theorem 5.3
(Analogue of theorem 2.4) If
, suffix is of L-type, and suffix is of S-type, then .
Proof
Consider the proof of theorem 2.4. It applies to any strings which have prefixes of the form [>= 1 occurrences of b][1 occurrence of a]... and [>= 1 occurrences of b][1 occurrence of c]... respectively. We know that
Since
The same argument applies to
Therefore, the two must have the same form and the proof of theorem 2.4 applies.
Consequently,
- First, we have L-type suffixes that start with symbol
. - Then, we have S-type suffixes that start with symbol
. - Then, L-type suffixes that start with symbol
. - Then, S-type suffixes that start with symbol
. - Then, L-type suffixes that start with symbol
. - Then, S-type suffixes that start with symbol
. - ...and so on
Now, let's try to adapt our old procedure 2 to produce
Procedure 5
C = [-1] * (n + 1)
put {T[i, i], i} for all LMS-type i into heap H, which uses the alex comparator
# just like our previous extend2 function, except here we insert T[j, E[j]] into the heap
def extend2(i):
j = i - 1
if (j >= 0) and (position j is L-type)
place j in the first empty position of the bucket of T[j] in C
put {T[j, E[j]], j} into H
while H is not empty:
pop out {v, i}, the smallest element of H
extend2(i)
The proof for procedure 5 correctly filling the L-regions of
First, notice that the deletion of any string from the heap can only result in the insertion of a strictly larger substring under alex (or no insertion at all):
- If the string being deleted is
with LMS , then by theorem 5.1, we have . - Otherwise,
( is L-type) being deleted results in the insertion of , then (theorem 5.2).
Since we always erase the alex-smallest substring from the heap, the order in which we delete LMS substrings from the heap is alex-non-decreasing. Coverage (every L-type LMS substring is inserted into the heap at some point) is also guaranteed for the same reason as before.
Now, consider any two L-type LMS substrings
- If
, then their relative order doesn't matter. - Otherwise, if
, WLOG assume . Then we necessarily have , and the two are placed in different buckets, with the former's bucket lying to the left of the latter's. - Otherwise, if
, then and will be placed in the same bucket. Recall that for any character and strings , we have . Therefore, . Note that we cannot have here. Consider the other two cases- If
, then the former is deleted before the latter from the heap, and is placed in their common bucket before . Since we also have here, their relative order in is valid. - If
, then the former is deleted before the latter from the heap, and is placed in their common bucket before . Since we also have here, their relative order in is valid.
- If
This implies
The observant reader will have noticed that our arguments have been almost identical to the initial use of L-induce. We shall continue in a similar vein. Consider this optimised version of procedure 5 (adapted from procedure 2.5):
Procedure 5.5
C = [-1] * (n + 1)
put {T[i, i], i} for all LMS-type i into heap H
# once again, we don't place anything into the heap any more
def extend2(i):
j = i - 1
if (j >= 0) and (position j is L-type)
place j in the first empty position of the bucket of T[j] in C
# we iterate over the first character of the suffix
for character c from 0 to n:
# we first process the L-region of c
let C[l, r] be the L-region for c
for i from l to r:
extend2(C[i])
# we then process the LMS-types of c, naively
while H is not empty and the smallest string in H begins with c:
pop out {T[i, i], i}, the smallest element of H
extend2(i)
Why is this correct? The proof is almost identical to that of procedure 2.5 - extend2(i) is called over the same sequence of
Now, recall that at this point in the old L-induce, we revealed the need for
This allows us to easily optimise procedure 5.5 to the following
Procedure 6
// Given n, T, Q, and the bucket boundaries (see code in theorem 2.4)
int n;
vector<int> T, Q, head, tail;
vector<int> C(n + 1, -1);
// Seeding - we put LMS positions at the tails of their S-buckets
for(int i = 0; i <= n; i ++)
if(is_LMS_type(Q[i]))
C[--tail[T[i]]] = i;
auto extend2 = [&](int i) -> void
{
int j = i - 1;
if(j >= 0 and is_L_type(Q[j]))
{
C[head[T[j]]] = j;
head[T[j]] ++;
}
};
// induce L positions into C
auto l_induce = [&]() -> void
{
for(int i = 0; i <= n; i ++)
if(C[i] != -1)
extend2(C[i]);
};
l_induce();
// clean up LMS types from C
for(int i = 0; i <= n; i ++)
if(C[i] != -1 and is_LMS_type(Q[C[i]]))
C[i] = -1;
The justification of its correctness is basically identical to that of procedure 3, and is left as an exercise to the reader.
Now that we have filled the L-regions of
Once again, the proof is very similar to what we've done before (one change to note is that the position of an L-type
And... we end up with
6. Putting it all together
Let's finally assemble our algorithm from the pieces we've painstakingly built:
# T satisfies conditions from section 1
SA(T):
N = |T|
n = N - 1
if N == 1:
return [0]
classify L, S, LMS types in O(n)
#PHASE 1: sort LMS substrings under alex
C = [-1] * (n + 1)
seed C with T[i, i] for all LMS positions i, in O(n)
run L induce on C # we compute fresh bucket boundaries before every L and S induce
run S induce on C
#PHASE 2: construct T2 and sort its suffixes to get G
#label full LMS substrings
R = [-1] * (n + 1)
label = -1, last = -1
for i from 0 to n:
if C[i] is LMS type:
j = C[i]
if last == -1 or !equal(last, j): #equal(i, j) compares P[i] and P[j]
label += 1
R[j] = label
last = j
#construct T2 and sort its suffixes
T2 = []
for i from 0 to n:
if i is LMS type:
T2.append(R[i])
A2 = SA(T2)
#construct G from A2
G = A2
for i from 0 to |G| - 1:
G[i] = l[G[i]] #l[i] is i-th LMS position in T
#PHASE 3: seed A with G and run the final L and S induce
A = [-1] * (n + 1)
seed A with G
run L induce on A
run S induce on A
return A
My own implementation of SAIS was unfortunately created before I wrote this blog, and doesn't really follow the exposition, but here's a highly readable one by my friend Opus 5.5 that does:
Code
#include <bits/stdc++.h>
using namespace std;
// Types of positions (section 2, 3). An LMS position is also S-type.
enum Type : uint8_t { L_TYPE, S_TYPE, LMS_TYPE };
// Computes the suffix array of T.
// T must satisfy the conditions from section 1: with n = |T| - 1,
// - T[n] == 0 is the sentinel (the unique minimum), and
// - T[i] lies in [1, n] for all i < n.
vector<int> SA(const vector<int> &T)
{
const int N = (int)T.size(), n = N - 1;
// base case
if(N == 1)
return {0};
// ---------------------------------------------------------------
// Classify positions as L / S / LMS (theorem 2.3, section 3)
// ---------------------------------------------------------------
vector<Type> Q(N);
Q[n] = S_TYPE;
for(int i = n - 1; i >= 0; i --)
{
if(T[i] < T[i + 1])
Q[i] = S_TYPE;
else if(T[i] > T[i + 1])
Q[i] = L_TYPE;
else
Q[i] = Q[i + 1];
}
vector<int> l; // l[k] = k-th LMS position, so l = {l_0 < l_1 < ... < l_{m - 1}}
for(int i = 1; i <= n; i ++)
if(Q[i] == S_TYPE and Q[i - 1] == L_TYPE)
{
Q[i] = LMS_TYPE;
l.push_back(i);
}
const int m = (int)l.size();
auto is_L = [&](int i) { return Q[i] == L_TYPE; };
auto is_S = [&](int i) { return Q[i] != L_TYPE; };
auto is_LMS = [&](int i) { return Q[i] == LMS_TYPE; };
// ---------------------------------------------------------------
// Bucket boundaries (section 2): the bucket for symbol c is [head[c], tail[c])
// ---------------------------------------------------------------
vector<int> cnt(N, 0); // symbols lie in [0, n]
for(int x : T)
cnt[x] ++;
// induce L/S both modify the boundaries, so we hand out fresh copies every time
auto get_buckets = [&](vector<int> &head, vector<int> &tail) -> void
{
head.assign(N, 0);
tail.assign(N, 0);
for(int c = 0, sum = 0; c < N; c ++)
{
head[c] = sum;
sum += cnt[c];
tail[c] = sum;
}
};
// ---------------------------------------------------------------
// Induced sorting (section 4)
//
// `seeds` contains the LMS positions, in sorted order. Returns the array
// obtained by seeding them at the tails of their S-buckets, then running
// L-induce (procedure 3) followed by S-induce (procedure 4).
//
// Phase 1 and phase 3 differ *only* in the seeds they pass here.
// ---------------------------------------------------------------
auto induced_sort = [&](const vector<int> &seeds) -> vector<int>
{
vector<int> A(N, -1);
vector<int> head, tail;
// seeding: walk the seeds from largest to smallest, filling each S-bucket from its tail
get_buckets(head, tail);
for(int k = (int)seeds.size() - 1; k >= 0; k --)
{
int i = seeds[k];
A[-- tail[T[i]]] = i;
}
// L-induce: walk left to right; if A[i] - 1 is L-type, append it to the
// first free slot of its L-bucket (this is extend2)
for(int i = 0; i < N; i ++)
{
if(A[i] == -1)
continue;
int j = A[i] - 1;
if(j >= 0 and is_L(j))
A[head[T[j]] ++] = j;
}
// clean up: remove the seeded LMS positions, the S-induce places them again
for(int i = 0; i < N; i ++)
if(A[i] != -1 and is_LMS(A[i]))
A[i] = -1;
// S-induce: walk right to left; if A[i] - 1 is S-type, prepend it to the
// last free slot of its S-bucket (this is extend3)
get_buckets(head, tail);
A[0] = n; // the sentinel is the smallest suffix, place it manually
for(int i = n; i >= 0; i --)
{
if(A[i] == -1)
continue;
int j = A[i] - 1;
if(j >= 0 and is_S(j))
A[-- tail[T[j]]] = j;
}
return A;
};
// ---------------------------------------------------------------
// PHASE 1: sort all LMS substrings under alex (section 5.2)
//
// Seeding with T[i, i] for each LMS position i: these are single
// characters, so any order works, as long as each one lands in its own
// bucket (procedure 6).
// ---------------------------------------------------------------
vector<int> C = induced_sort(l);
// ---------------------------------------------------------------
// PHASE 2: label full LMS substrings, build T2, recurse (sections 5.1, 6)
// ---------------------------------------------------------------
// equal(i, j) checks whether P_i == P_j for LMS positions i != j.
// P_i = T[i, E_i] ends at the next LMS position, so we walk both substrings
// together until one of them ends. They are equal iff both end at the same
// offset with all characters equal along the way.
// (If one of them is the sentinel n, the very first characters differ, so
// we never read past the end of T.)
auto equal = [&](int i, int j) -> bool
{
for(int k = 0; ; k ++)
{
if(T[i + k] != T[j + k])
return false;
bool end_i = (k > 0 and is_LMS(i + k));
bool end_j = (k > 0 and is_LMS(j + k));
if(end_i or end_j)
return end_i and end_j;
}
};
// walk C (which is alex-sorted) and give equal full LMS substrings equal labels
// R[i] = label of P_i, for LMS positions i
vector<int> R(N, -1);
int label = -1, last = -1;
for(int k = 0; k < N; k ++)
{
int j = C[k];
if(!is_LMS(j))
continue;
if(last == -1 or !equal(last, j))
label ++;
R[j] = label;
last = j;
}
// C[0] = n is the first LMS position seen, so the sentinel gets label 0,
// and every other label lies in [1, m - 1]: T2 satisfies section 1
// T2[k] = label of the k-th full LMS substring
vector<int> T2(m);
for(int k = 0; k < m; k ++)
T2[k] = R[l[k]];
// G = LMS positions in sorted order of their suffixes
vector<int> G = SA(T2);
for(int k = 0; k < m; k ++)
G[k] = l[G[k]];
// ---------------------------------------------------------------
// PHASE 3: seed with G and induce the actual suffix array (section 4)
// ---------------------------------------------------------------
return induced_sort(G);
}
// Convenience wrapper for arbitrary non-negative input: s[i] in [0, K).
// Remaps s to satisfy section 1 in O(n + K), and drops the sentinel from the result.
vector<int> suffix_array(const vector<int> &s, int K)
{
const int n = (int)s.size();
// rank[x] = 1 + number of distinct symbols in s smaller than x
vector<int> rank(K, 0);
for(int x : s)
rank[x] = 1;
for(int x = 0, r = 0; x < K; x ++)
if(rank[x])
rank[x] = ++ r;
vector<int> T(n + 1);
for(int i = 0; i < n; i ++)
T[i] = rank[s[i]];
T[n] = 0;
vector<int> A = SA(T);
return vector<int>(A.begin() + 1, A.end()); // A[0] is always the sentinel
}