后缀数组(SA)习记
前言
在学习 之前,有必要复习一下什么是 计数排序、基数排序、桶排序 以及三者之间的区别。
然后细说一下 后缀数组 的三种构造方式 倍增、DC3、SA_IS
部分代码和内容转自:
如侵权可联系删除。
目录:
计数排序
计数排序(Counting sort)是一种线性时间的排序算法。
算法流程
- 计算每个数出现了几次;
- 求出每个数出现次数的 前缀和;
- 利用出现次数的前缀和,从右至左计算每个数的排名。
算法分析
- 是一种稳定的排序算法。
- 时间复杂度为 , w 为值域大小
缺点:
- 时,不如 的排序算法。
代码实现
基数排序
基数排序(Radix sort)是一种非比较型的排序算法,最早用于解决卡片排序的问题
算法流程
将待排序的元素拆分为 个关键字。先对第 个关键字进行稳定排序,然后对第 个关键字稳定排序,直到对第 个关键字排序完成。
- 基数排序主要是一种思想,对某个关键字的内部排序是依靠其他排序算法来完成。如计数排序。
算法分析
- 是一种稳定的排序算法。
- 时间复杂度: 一般来说,如果每个关键字的值域都不大,就可以使用 计数排序 作为内层排序,此时的复杂度为 ,其中 为第 关键字的值域大小。如果关键字值域很大,就可以直接使用基于比较的 排序而无需使用基数排序了。
- 空间复杂度: ;
代码实现
桶排序
桶排序(Bucket sort)是排序算法的一种,适用于待排序数据值域较大但分布比较均匀的情况。
算法流程
- 将值域分块,设置一个定量的数组当作空桶,一个捅对应一个块的元素
- 遍历序列,并将每个块中元素一个个放到对应的桶中;
- 对每个不是空的桶进行排序,通常是插入排序;
- 从不是空的桶里把元素再放回原来的序列中。
算法分析
- 稳定性:
- 如果使用稳定的内层排序,并且将元素插入桶中时不改变元素间的相对顺序,那么桶排序就是一种稳定的排序算法。
- 由于每块元素不多,一般使用插入排序。此时桶排序是一种稳定的排序算法。
- 时间复杂度:
- 桶排序的平均时间复杂度为 (将值域平均分成 块 + 排序 + 重新合并元素),当 时为 .
- 桶排序的最坏时间复杂度为 。
代码实现
SA基本概念及性质
SA基本概念
- 后缀:
- 字典序:从左往右找两个字符串第一个不同字母,空字符设为最小。
- 后缀排序:将所有后缀 看作独立的串,放在一起按照字典序进行升序排序。
- 后缀排名 : 表示后缀 在后缀排序中的排名,即他是第几小的后缀。
- 后缀数组 : 表示排名第 小的后缀。
- LCP: Longest Common Prefix, 最长公共前缀。
一个重要的等式:rk[sa[i]] = sa[rk[i]] i
。
Naive 求法。
对所有后缀字符串进行 std::sort
,用 哈希 + 二分 重写 cmp()
比较两个后缀的 LCP 字典序大小, 时间复杂度为 , 同时哈希检测次数达到了 ,非常容易冲突。 显然不是理想的算法。
LCP 最长公共前缀
问:设有一组排序过的字符串 。如何快速的求任意 与 的 LCP?
需要一个关于 LCP 的"区间可加性":
对于任意的 ,
故有:
证明:
- 令
- 时
- 由于 ,所以 。
- 则
- 时
- 已知 , 且字典序 ,所以 ,所以 ,结论同样成立
Height 数组
SA 中非常重要的数组
定义: , 排名为 的后缀与排名为 的后缀的 LCP 长度, 特别地,height[1] = 0
显然有了 Height 数组,刚才的问题就变成了 区间最小值查询 啦!
那么如何求 Height ?
引理:
展开引理: ,省略 (S[])。
等价于:
因此,不妨设 ,表示后缀 与排名比他小 的后缀的 LCP
即证:
令 , 。
则 ,
- 当 时
- 显然成立。
- 当 时
- 对于 去首字母后变为 ,字典序关系是不变的。, 且
- 而 , 且中间没有其他后缀,所以有 。
- 故 , 由“区间可加性”得和上式得
- 结论依然成立,则 成立
代码实现
倍增法构造SA
思路
将 比较字典序的二分求 LCP 转化为倍增求 LCP。
首先等效的认为在字符串的末尾增添无限个空字符 \0
按照通常的倍增思路:
- 定义 ,即以 位置开头,长度为 的子串。
- 后缀 与 的字典序关系等价于 与 的字典序关系。
- 事实上,只需要将 , 排序即可。
然后可以倍增的进行排序
- 假设当前已经得到了 的排序结果,
与 ,思考如何利用它们排序 。 - 由于 是由 和 前后拼接而成。
- 因此比较 与 字典序可以转化为先比较 与 ,
- 再比较 与 。
- 因此可以将 看作一个两位数,高位是 ,低位是 。
显然有 的做法
而对于两位数的排序,我们有基数排序!
- 将他们排序时,需要先按照高位排序,高位相同时,按照低位排序。此过程为基数排序
- 实际代码运行时,先进行的是低位的排序。
此时借用一下,葫芦爷的课件!其实一直都在借用
算法分析
时间复杂度:
- 总共需要运行 轮,每轮使用基数排序,复杂度为 , 整体复杂度为
代码实现
直接干!
上述代码可以进行一些常数优化,即:
第二关键字无需计数排序
优化计数排序值域
- 每次对 进行去重之后,我们都计算了一个 ,这个 即是 的值域,将值域赋值为
- 将 rk[id[i]] 存下来,减少不连续内存访问, 这个优化在数据范围较大时效果非常明显。这个模板
若排名都不相同直接生成后缀数组
考虑新的 数组,若其值域为 那么每个排名都不同,此时无需再排序。
常数优化版
DC3 构造SA
待填
SA_IS
待填
应用
SA 数组性质应用
实现字符串最小表示
将字符串 复制一份变成 ,找长度大于 字典序最小的的后缀对应位置就是最小表示位置
字符串中查找子串
- 在线地在主串 中寻找模式串
- 子串一定是是某个后缀的前缀。先跑一次 SA, 可以在 个后缀中按照和 的字典序关系进行二分查找,就可以找到是否出现。
- 如果子串出现多次,如果多次出现可以再次二分查找,两次二分查找可以分别找出在 SA 中的最左位置和最右位置,并且可以依次得到出现位置。
从字符串首尾取字符最小化字典序
- 优化暴力,每次从正尾取是看正串和反串谁小,那么我们就把字符串拼成正串+反串,跑一次 SA。
- 记录首尾位置,比较对应的“后缀”字典序即可!
例题-[USACO007DEC]Best Cow Line G
Height 数组性质应用
求最长公共子串
-
求 两串最长公共子串,先将两串用分割符拼接起来。
-
遍历 在 中的所有位置,找到第一个小于 i 和第一个大于 i 的 s 的两个后缀,对应和 t[i] 取最长 LCP 即可,即用数据结构来查最值即可$。
-
求本质不同公共子串个数
- 类似上面的求法,只不过在每次计算的时候需要减去 T 的后缀 与上一个 T 的后缀的 LCP,然后取与 0 取max
比较字符串两个子串大小关系
- 若比较 大小关系
- 如果 ,则
A<B <-> |A| < |B|
- 其中一个是另一个的前缀,长度小的字典序一定不会大于长度大的
- 否则
A < B <-> rk[a] < rk[c]
- 从两串下标为 的位置开始看,谁小谁字典序更小
求本质不同子串数目
- 按字典序从小到大枚举所有后缀,统计有多少个新出现的前缀即可。
- 对于排名第 i 的后缀 ,共有 个前缀,其中有
个前缀同时出现在前一个排名的后缀 中,因此减掉即可。 - 上述证明是不完整的,还需要证明所有在 S[sa[i], n] 中出现,但没有在
中出现的前缀,他们在所有更小排名的后缀串也都没有出
现。 - 证明就是,如果出现过会破坏我们求 时的性质。
- 求本质不同同构子串数目(字符集大小为 )
- 枚举所有字符集转换,共有 种。将形成的 种字符串通过不同的分隔符分割,大串跑 SA
- 统计大串 S 的本质不同子串数目,观察发现子串中有 种不同字符时会在 S 中出现 次总次数为 ,如果是只有一种字符只会出现 次总次数为 ,因此计算时需要将 补到 6 次进行计算,统计全 串只需要一个 for 循环记录最长连续相同子串即可记为 ,因此答案为 。
- 计算答案前还需要减去包含分隔符的子串,类似双指针的思想
ans -= (n + 1)*(len - i + 1)
查找出现 次的子串的最大长度。
- 一个字符串出现 次表明在相邻的 个 height 数组中均出现。
- 那么只需要查找相邻 个 height 数组的最小值的最大值即可,单调队列能 , 也可以用区间最值查询。
洛谷P2852 [USACO06DEC]Milk Patterns G
是否有某字符串在文本串中至少不重叠地出现了两次
可以二分目标串的长度 ,将 数组划分成若干个连续 LCP 大于等于 的段,利用 RMQ 对每个段求其中出现的数中最大和最小的下标,若这两个下标的距离满足条件,则一定有长度为 的字符串不重叠地出现了两次。
连续的若干个相同子串
- 询问 多少个子串满足优秀拆分,即拆分为 形式的串。
- 按贡献考虑,观察 AA 和 BB 交界,记录 分别代表 i 为 AA 串结尾, i 为 BB(与AA同理)串开头。
- 那么最终答案是
- 加哈希可以拿 分,SA 正解参见 AcFunction's blog
- 第一次做感觉边界没有搞得太明白。。同时注意下多组数据清空的问题。
所有后缀之间 的加和。
- 即求
height[]
数组所有区间的最小值加和。 - 定义状态
f[i]
表示前缀 的所有后缀区间的最小值之和。 - 考虑
height[i]
与height[i-1]
的关系:- 如果 height[i] >= height[i - 1],f[i] 能取到 f[i - 1] 的所有值
- 反之,height[i] 不能取到 f[i - 1] 的值,要往前继续找 小于等于 height[i] 的下标。
- 上点可以用单调栈来维护左边第一个小于 height[i] 的下标。
询问不超过 k 次修改单个字符的连续子串匹配个数
- 给定两个字符串 ,询问对于 中所有子串,有多少个长度为 的联系子串满足不超过 次修改,能变成
- 将 T 接在 S 的后面,跑一次 SA,然后用最多进行 次求 LCP 模拟匹配。
参考资料
__EOF__

本文链接:https://www.cnblogs.com/Roshin/p/SA_notes.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人