后缀数组学习笔记
一、定义
对于下标从
对于后缀数组,我们定义
例如对于字符串 aabaaab
,它有
aaab
aab
aabaaab
ab
abaaab
b
baaab
那么容易得到
容易发现的是
二、后缀数组的求解
1. 暴力
直接预处理出
2. 倍增
(1) 倍增求解后缀数组
考虑我们比较两个字符串大小的过程。考察对于两个长度为
- 或是
且 。
那么我们将长度为
于是让我们描述这个做法的过程:
- 首先对每个字符排序,得到
。 - 套用上面的过程,用
作为第一、二关键字排序,得到长度为 的子串的排名。 - 每次都这样做,用长度为
的子串的排名得到长度为 的子串的排名。 - 考虑这样做的疏漏:对于
,这个下标存在 的情形,此时视为 即可。对于长度 的后缀,并不存在子串 ,此时将子串视为 。 - 当
时,此时的 就是所求。
对于复杂度的分析,倍增是
(2) 基数排序优化
考虑复杂度的可优化部分:倍增是难以优化的,但是对于排序,我们可以浅浅利用一番其性质。注意到每次排序的是一个二元组,于是考虑进行基数排序优化。那么让我们插播一下基数排序的内容。
基数排序是建立在桶排序的基础之上的。本质上是一个根据优先级逐层确定顺序的过程。对于一些正整数的排序过程是这样的:
- 先按照个位的不同排序,放入桶中;
- 再按照十位的不同排序,但要注意放入桶中的顺序是个位排序后的顺序;
- 依次排完所有十进制位。
对于这样做的正确性,事实上体现着基数排序的核心就是多关键字的排序。对于复杂度分析,令
那么对于后缀数组的排序,
(3) 一些常数优化
- 第二关键字无需桶排序。原因是对于
,它们的值一定是 ,一定最小,直接扔到数组最前面即可。对于剩下的后缀,有 ,于是放入和它拼接而成的 即可。 - 每次基数排序的值域设为
的值域即可。 - 若
的值域已经为 ,显然可以直接跳出。
代码:
void SA(int M) {
int m = M, p = 0;
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) {
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;
for (int i = 1; i <= m; i++) cnt[i] = 0;
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];
swap(lk, rk);
p = 0;
for (int i = 1; i <= n; i++) {
if (lk[sa[i]] == lk[sa[i - 1]] && lk[sa[i] + w] == lk[sa[i - 1] + w]) rk[sa[i]] = p;
else rk[sa[i]] = ++p;
}
if (p == n) break;
}
}
其中 M
是初始的值域范围,由题意得到。
然而只会求后缀数组并没有什么用处,我们还需要知道一些奇妙的性质。
三、 的一些性质
性质
我们记
对于
,此时显然第 位的 不同,且 。 ,此时 。 ,此时 。
也就是无论如何,
性质
这样一条性质由性质
性质
这个性质可以通过
四、 数组
1. 定义
我们定义
定义
2. 求法
引理:
。
考虑证明。若
根据定义
于是我们给出暴力求后缀数组的实现:
void H() {
for (int i = 1, k = 0; i <= n; i++) {
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
h[rk[i]] = k;
}
}
五、后缀数组的应用
1. 求字符串中两子串最长公共前缀
根据上文的性质
于是这个问题转化为了 RMQ 问题,ST 表求解即可。
2. 求字符串中本质不同子串的数目
子串也就是后缀的前缀,于是枚举后缀,计算前缀的总数,再减掉重复的。总数其实就是子串的个数,也就是
3. 出现至少 次的子串的最大长度
出现至少
4. 最长公共子串
后缀数组一个常见的套路就是把一堆子串拼在一起。于是我们将所有的子串拼在一起就变成了问题
- 一些个字符串间互相连接形成干扰。于是在每两个字符串中间加上一个分割符号即可。注意最后一个字符串末尾不加这个符号。
- 出现了
次并不完全等价于是最长公共子串,需要判定是否在 个字符串中都出现过。具体的实现上可以双指针去做,过程中动态开桶维护出现的次数, 显然是单调不降的,于是 ST 表维护即可。
5. 最长回文子串
套路的做法是将字符串反转,在中间插入一个字符后加入反转后的字符串。这样一来转化为了
需要注意的是这样做需要保证后缀之间没有干扰,因此需要插入一个大于最大值的字符。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!