参考:https://cp-algorithms.com/string/suffix-array.html
后缀数组指把一个字符串的所有后缀按照字典序排序得到的数组。
以下默认下标从 \(0\) 开始,令 S[i...] 为字符串从 i 开始的后缀。
sa[i]: 第 \(i\) 大的后缀的起始位置。
例:ababc 的后缀从小到大排序为:空后缀,ababc,abc,babc,bc,c,其后缀数组为 [5, 0, 2, 1, 3]。
lcp[i]:后缀数组中相邻两个后缀的最长公共前缀组成的数组,S[sa[i]...] 和 S[sa[i+1]...] 的最长公共前缀为 lcp[i]。
sa 的计算
倍增计算,复杂度 \(O(n \lg^2 n)\) 或 \(O(n \lg n)\) (优化排序部分)。
const int MN = 400010;
int n, k;
int rsa[MN], tmp[MN];
bool cmp(int x, int y) {
if (rsa[x] != rsa[y]) return rsa[x] < rsa[y];
int rx = x + k <= n ? rsa[x + k] : -1;
int ry = y + k <= n ? rsa[y + k] : -1;
return rx < ry;
}
template <class S> vector<int> exec(const S &s) {
n = int(s.size());
vector<int> sa(n + 1);
for (int i = 0; i <= n; i++) {
sa[i] = i;
rsa[i] = i < n ? s[i] : -1;
}
for (k = 1; k <= n; k *= 2) {
sort(sa.begin(), sa.end(), cmp); // 用计数排序可以优化这部分
tmp[sa[0]] = 0;
for (int i = 1; i <= n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] + int(cmp(sa[i - 1], sa[i]));
}
copy(tmp, tmp + n + 1, rsa);
}
return sa;
}
lcp 的计算
令 rank[i] 为后缀 i 在后缀数组的排名。
从位置为 \(0\) 的后缀开始从前往后依次计算 S[sa[rank[i]-1]...] 与 S[i...] 的 lcp。
计 k=sa[rank[i]-1],假设已经求得了 S[k...] 和 S[i...] 的 lcp 为 L,那么 S[k+1...] 和 S[i+1...] 的 lcp 一定不小于 L-1,而 S[sa[rank[i+1]-1]...] 与 S[i+1...] 的 lcp 应该还要比这个更长,于是可以用一个类似双指针的方法 \(O(n)\) 计算所有 lcp[i]。
lcp 数组可以与 Sparse Table 配合使用,要计算后缀数组内任意两个串 i 和 j 的最长公共前缀,只需要计算 lcp[rank[i]],lcp[rank[i]+1],...,lcp[rank[j]-1] 的最小值。