一些约定
字符串下标从 1 开始.
字符串 s 的长度为 n.
" 后缀 i" 代指以第 i 个字符开头的后缀,存储时用 i 代表字符串 s 的后缀 s[i…n].
后缀数组是什么?
后缀数组(Suffix Array)主要关系到两个数组:sa 和 rk.
其中,sa[i] 表示将所有后缀排序后第 i 小的后缀的编号,也是所说的后缀数组,后文也称编号数组 sa;
rk[i] 表示后缀 i 的排名,是重要的辅助数组,后文也称排名数组 rk.
这两个数组满足性质:sa[rk[i]]=rk[sa[i]]=i.
解释
后缀数组示例:

后缀数组怎么求?
O(n^2logn) 做法
相信这个做法大家还是能自己想到的:将盛有全部后缀字符串的数组进行 sort 排序,由于排序进行 O(nlogn) 次字符串比较,每次字符串比较要 O(n) 次字符比较,所以这个排序是 O(n2logn) 的时间复杂度.
O(nlog^2n) 做法
这个做法要用到倍增的思想.
首先对字符串 s 的所有长度为 1 的子串,即每个字符进行排序,得到排序后的编号数组 sa1 和排名数组 rk1.
倍增过程:
-
用两个长度为 1 的子串的排名,即 rk1[i] 和 rk1[i+1],作为排序的第一第二关键字,就可以对字符串 s 的每个长度为 2 的子串:{s[i…min(i+1,n)] ∣ i∈[1, n]} 进行排序,得到 sa2 和 rk2;
-
之后用两个长度为 2 的子串的排名,即 rk2[i] 和 rk2[i+2],作为排序的第一第二关键字,就可以对字符串 s 的每个长度为 4 的子串:{s[i…min(i+3,n)] ∣ i∈[1, n]} 进行排序,得到 sa4 和 rk4;
-
以此倍增,用长度为 w/2 的子串的排名,即 rkw/2[i] 和 rkw/2[i+w/2],作为排序的第一第二关键字,就可以对字符串 s 的每个长度为 w 的子串 s[i…min(i+w−1, n)] 进行排序,得到 saw 和 rkw.其中,类似字母序排序规则,当 i+w>n 时,rkw[i+w] 视为无穷小;
-
rkw[i] 即是子串 s[i…i+w−1] 的排名,这样当 w⩾n 时,得到的编号数组 saw,也就是我们需要的后缀数组.
过程
倍增排序示意图:

显然倍增的过程是 O(logn),而每次倍增用 sort 对子串进行排序是 O(nlogn),而每次子串的比较花费 2 次字符比较;
除此之外,每次倍增在 sort 排序完后,还有额外的 O(n) 时间复杂度的,更新 rk 的操作,但是相对于 O(nlogn) 被忽略不计;
所以这个算法的时间复杂度就是 O(nlog2n).
```cpp
#include
#include
#include
#include
using namespace std;
constexpr int N = 1000010;
char s[N];
int n, w, sa[N], rk[N << 1], oldrk[N << 1];
// 为了防止访问 rk[i+w] 导致数组越界,开两倍数组.
// 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些.
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];
}); // 这里用到了 lambda
memcpy(oldrk, rk, sizeof(rk));
// 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
// 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
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;
}
```
O(nlogn) 做法
在刚刚的 O(nlog2n) 做法中,单次排序是 O(nlogn) 的,如果能 O(n) 排序,就能 O(nlogn) 计算后缀数组了.
前置知识:[计数排序][基数排序]
由于计算后缀数组的过程中排序的关键字是排名,值域为 O(n),并且是一个双关键字的排序,可以使用基数排序优化至 O(n).
```cpp
#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) {
// 对第二关键字:id[i] + w进行计数排序
memset(cnt, 0, sizeof(cnt));
memcpy(id + 1, sa + 1,
n * sizeof(int)); // id保存一份儿sa的拷贝,实质上就相当于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];
// 对第一关键字: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;
}
```
一些常数优化
如果你把上面那份代码交到 LOJ #111: 后缀排序 上:

这是因为,上面那份代码的常数的确很大.
第二关键字无需计数排序
思考一下第二关键字排序的实质,其实就是把超出字符串范围(即 sa[i]+w>n)的 sa[i] 放到 sa 数组头部,然后把剩下的依原顺序放入:
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;
优化计数排序的值域
每次对 rk 进行更新之后,我们都计算了一个 p,这个 p 即是 rk 的值域,将值域改成它即可.
若排名都不相同可直接生成后缀数组
考虑新的 rk 数组,若其值域为 [1,n] 那么每个排名都不同,此时无需再排序.
```cpp
#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 即为值域优化
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; // p = n 时无需再排序
}
for (int i = 1; i <= n; i++) printf("%d ", sa[i]);
return 0;
}
```
## 一些约定
字符串下标从 $1$ 开始.
字符串 $s$ 的长度为 $n$.
" 后缀 $i$" 代指以第 $i$ 个字符开头的后缀,存储时用 $i$ 代表字符串 $s$ 的后缀 $s[i\dots n]$.
## 后缀数组是什么?
后缀数组(Suffix Array)主要关系到两个数组:$sa$ 和 $rk$.
其中,$sa[i]$ 表示将所有后缀排序后第 $i$ 小的后缀的编号,也是所说的后缀数组,后文也称编号数组 $sa$;
$rk[i]$ 表示后缀 $i$ 的排名,是重要的辅助数组,后文也称排名数组 $rk$.
这两个数组满足性质:$sa[rk[i]]=rk[sa[i]]=i$.
### 解释
后缀数组示例:

## 后缀数组怎么求?
### O(n^2logn) 做法
相信这个做法大家还是能自己想到的:将盛有全部后缀字符串的数组进行 `sort` 排序,由于排序进行 $O(n\log n)$ 次字符串比较,每次字符串比较要 $O(n)$ 次字符比较,所以这个排序是 $O(n^2\log n)$ 的时间复杂度.
### O(nlog^2n) 做法
这个做法要用到倍增的思想.
首先对字符串 $s$ 的所有长度为 $1$ 的子串,即每个字符进行排序,得到排序后的编号数组 $sa_1$ 和排名数组 $rk_1$.
倍增过程:
1. 用两个长度为 $1$ 的子串的排名,即 $rk_1[i]$ 和 $rk_1[i+1]$,作为排序的第一第二关键字,就可以对字符串 $s$ 的每个长度为 $2$ 的子串:$\{s[i\dots \min(i+1, n)]\ |\ i \in [1,\ n]\}$ 进行排序,得到 $sa_2$ 和 $rk_2$;
2. 之后用两个长度为 $2$ 的子串的排名,即 $rk_2[i]$ 和 $rk_2[i+2]$,作为排序的第一第二关键字,就可以对字符串 $s$ 的每个长度为 $4$ 的子串:$\{s[i\dots \min(i+3, n)]\ |\ i \in [1,\ n]\}$ 进行排序,得到 $sa_4$ 和 $rk_4$;
3. 以此倍增,用长度为 $w/2$ 的子串的排名,即 $rk_{w/2}[i]$ 和 $rk_{w/2}[i+w/2]$,作为排序的第一第二关键字,就可以对字符串 $s$ 的每个长度为 $w$ 的子串 $s[i\dots \min(i+w-1,\ n)]$ 进行排序,得到 $sa_w$ 和 $rk_w$.其中,类似字母序排序规则,当 $i+w>n$ 时,$rk_w[i+w]$ 视为无穷小;
4. $rk_w[i]$ 即是子串 $s[i\dots i + w - 1]$ 的排名,这样当 $w \geqslant n$ 时,得到的编号数组 $sa_w$,也就是我们需要的后缀数组.
#### 过程
倍增排序示意图:

显然倍增的过程是 $O(\log n)$,而每次倍增用 `sort` 对子串进行排序是 $O(n\log n)$,而每次子串的比较花费 $2$ 次字符比较;
除此之外,每次倍增在 `sort` 排序完后,还有额外的 $O(n)$ 时间复杂度的,更新 $rk$ 的操作,但是相对于 $O(n\log n)$ 被忽略不计;
所以这个算法的时间复杂度就是 $O(n\log^2n)$.
```cpp
#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];
// 为了防止访问 rk[i+w] 导致数组越界,开两倍数组.
// 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些.
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];
}); // 这里用到了 lambda
memcpy(oldrk, rk, sizeof(rk));
// 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
// 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
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;
}
```
### O(nlogn) 做法
在刚刚的 $O(n\log^2n)$ 做法中,单次排序是 $O(n\log n)$ 的,如果能 $O(n)$ 排序,就能 $O(n\log n)$ 计算后缀数组了.
前置知识:[计数排序][基数排序]
由于计算后缀数组的过程中排序的关键字是排名,值域为 $O(n)$,并且是一个双关键字的排序,可以使用基数排序优化至 $O(n)$.
```cpp
#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) {
// 对第二关键字:id[i] + w进行计数排序
memset(cnt, 0, sizeof(cnt));
memcpy(id + 1, sa + 1,
n * sizeof(int)); // id保存一份儿sa的拷贝,实质上就相当于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];
// 对第一关键字: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;
}
```
### 一些常数优化
如果你把上面那份代码交到 [LOJ #111: 后缀排序](https://loj.ac/problem/111) 上:

这是因为,上面那份代码的常数的确很大.
#### 第二关键字无需计数排序
思考一下第二关键字排序的实质,其实就是把超出字符串范围(即 $sa[i] + w > n$)的 $sa[i]$ 放到 $sa$ 数组头部,然后把剩下的依原顺序放入:
```cpp
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;
```
#### 优化计数排序的值域
每次对 $rk$ 进行更新之后,我们都计算了一个 $p$,这个 $p$ 即是 $rk$ 的值域,将值域改成它即可.
#### 若排名都不相同可直接生成后缀数组
考虑新的 $rk$ 数组,若其值域为 $[1,n]$ 那么每个排名都不同,此时无需再排序.
```cpp
#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 即为值域优化
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; // p = n 时无需再排序
}
for (int i = 1; i <= n; i++) printf("%d ", sa[i]);
return 0;
}
```