后缀全家桶 | 1. SA
本系列主要讲解:
- SA(后缀数组)
- SAM(后缀自动机)
- 广义 SAM
并结合例题,总结一些经典的套路。
(希望我可以尝试同时用 SA 和 SAM 解决一些例题)
(还好总结了,不然真的就全忘了)
算法讲解
约定
根据定义,两者互逆。
目标
使用小常数
求
1
使用 sort 进行排序,cmp 考虑二分最长公共前缀的位置,判断后一个位置大小关系。
实际测试中,优化过后的该算法可以通过大部分题目。
2
使用倍增加速排序。每次排序仅以
总共排序
其实,该思想出现的本质是因为有:
而
参考这个思想,我们看一道和后缀数组无关的题:(CF1654F)
- 给定一个长度为
,只包含小写字母的字符串 。 - 你可以将字符串的下标全部异或一个
的整数 ,即构造一个与 等长的新字符串 ,使得 。 - 最小化
的字典序,并输出字典序最小的 。 。
分析:设
有
双关键字排序也满足。
最后根据
for(int i = 0;i < (1 << n); ++i) rk[i] = c[i] - 'a';
for(int w = 0;w < n; ++w) {
for(int i = 0;i < (1 << n); ++i) A[i].first.first = rk[i], A[i].first.second = rk[i ^ (1 << w)], A[i].second = i;
sort(A, A + (1 << n));
for(int i = 0, p = 0;i < (1 << n); ++i)
if(i != 0 and A[i].first.first == A[i - 1].first.first and A[i].first.second == A[i - 1].first.second) rk[A[i].second] = p;
else rk[A[i].second] = ++p;
}
for(int i = 0;i < (1 << n); ++i) if(rk[i] == 1) {
for(int j = 0;j < (1 << n); ++j) putchar(c[j ^ i]);
return 0;
}
倍增不变,考虑将排序变为
这里真正的难点是理解
此处排序有双关键字,于是采用 LSD 基数排序 先对第二关键字排序。然后给一关键字排序使用的是计数排序。
模板
给出优化后的代码。注意开始要先计数排序一次。
bool cmp(int x, int y, int z) { return tmp[x] == tmp[y] and tmp[x + z] == tmp[y + z]; }
void Suf_Arr(int lenn) {
int mm = 127;
for(int i = 1;i <= lenn; ++i) ++cnt[rk[i] = s[i]];
for(int i = 1;i <= mm; ++i) cnt[i] += cnt[i - 1];
for(int i = lenn;i >= 1; --i) sa[cnt[rk[i]]--] = i;
for(int opt = 1;;opt <<= 1) {
int p = 0;
for(int i = lenn - opt + 1;i <= lenn; ++i) ID[++p] = i;
for(int i = 1;i <= lenn; ++i) if(sa[i] > opt) ID[++p] = sa[i] - opt;
for(int i = 1;i <= mm; ++i) cnt[i] = 0;
for(int i = 1;i <= lenn; ++i) ++cnt[sb[i] = rk[ID[i]]];
for(int i = 1;i <= mm; ++i) cnt[i] += cnt[i - 1];
for(int i = lenn;i >= 1; --i) sa[cnt[sb[i]]--] = ID[i];
memcpy(tmp, rk, sizeof tmp);
p = 0;
for(int i = 1;i <= lenn; ++i)
rk[sa[i]] = cmp(sa[i], sa[i - 1], opt) ? p : ++p;
if(p == lenn) break ; mm = p;
}
for(int i = 1;i <= lenn; ++i) rk[sa[i]] = i;
}
求
这才是 sa 解决的大多数问题都需要的东西。有了上面的预处理可以很轻松地完成。
结论:
据此时间复杂度可以做到
证明考虑表示出这几个后缀,这里不展开。
void Height() {
for(int i = 1, p = 0;i <= n; ++i) {
if(!rk[i]) continue ; if(p) --p;
while(s[i + p] == s[sa[rk[i] - 1] + p]) ++p;
h[rk[i]] = p;
}
}
求两后缀 lcp
结论:
, 。
这个我感觉比较显然。
所以我们维护 ST 表,即可
经典应用
不同子串的数目
考虑一个后缀
又显然
比较子串大小
假设在比较
若
否则,比较第
结合并查集/单调栈
见例题。
例题
其实很多 trick 都是前面留下的,跟 sa 无关。
「AHOI2013」差异
重点是求
由于
那么这就是单调栈经典问题,求出
「HAOI2016」找相同字符
给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。
Sol1
考虑容斥,答案为两个串拼在一起一共的不同子串方案减去各自单独成一个串的方案,统计部分类似上一题。(好像求的甚至是一样的)
Sol2
考虑直接做:从前往后扫,如果遇见
「JSOI2015」串分割
给定环形数字字符串
。把 进行 次切割,并分成 个非空的子串。每一个子串将其看成一个十进制数。最小化这 个数中的最大值。
首先最终答案各个数的位数一定是尽量相同的。可以二分后缀排名,然后贪心 check:
将原串复制一份,枚举起始点。现在考虑将额外的
如果
证明:如果可以加一,就直接加。最后一定可以找到合法解。
考虑下一次使用
「BZOJ4310」跳蚤
把
分成 组,然后对于每一组,取其字典序最大的子串,得到一个集合 ,记 中字典序最大的串为 ,询问 字典序最小可以是什么。
!模型:“二分这个子串。”
考虑对于
所以每个
得到这个子串的
=> 思考:为何要从后向前贪心?
=> 是否记得:之前模拟赛有一道关于最小字典序的题,从后向前 dp 避免了后效性。原因显然,可以到那个题去看。(决策正确性)
Fun Fact:上面两道题非常相似,一道限制了子串长度;另一道需要从后向前保证字典序。
「NOI2015」品酒大会
对每个
求有多少对后缀满足 以及满足条件的两个后缀的权值乘积的最大值。
因为我打的是单调栈所以讲一下单调栈做法。其实并查集和单调栈维护这一类信息是近似的。(但是并查集看着清晰得多,应用也更广)下题会具体介绍并查集做法。
考虑每个
单调栈维护一个结构体,这个结构体记录要维护的信息,然后对
在加一点关于基本功的细节:建议跑一次单调栈,每个
「SNOI2020」字符串
有两个长度为
的由小写字母组成的字符串 ,取出他们所有长为 的子串(各有 个),这些子串分别组成集合 。现在要修改 中的串,使得 和 完全相同。可以任意次选择修改 中一个串的一段后缀,花费为这段后缀的长度。总花费为每次修改花费之和,求总花费的最小值。
并查集!你过关!
这个题,单调栈似乎真的没了……因为单调栈只能维护连续序列的 max 区间……而并查集可以非常灵活地操作。
待补(((
「NOI2016」优秀的拆分
显然可以前后单独计算。
我们考虑先停一停,周末再写。
repeats
字符串识别
对于每一位,求出经过这一位的在
中仅出现过一次的子串的最短长度。
这个“仅出现过一次”的限制可以通过求
那么当前的
仅此而已吗?考虑这个后缀大于
然后线段树区改单查即可。
「BJOI2020」封印
给定
,多次询问 和 的最长公共子串长度。
考虑预处理出
只需要求
!模型:我们可以使用 二分+ST 解决:
二分答案
现在看一下预处理怎么搞,发现这个东西就是 rk 值与
我们按照
for(int i = 2;i <= len; ++i) {
tmp = min(tmp, h[i]); // 按照定义算 lcp
if(sa[i] > len2 + 1) dp[sa[i] - len2 - 1][0] = tmp; // 在 s 中,直接更新答案
else tmp = h[i + 1]; // 是一个 t,作为 lcp 计算的起点,更新
}
还有个小问题:实现的时候将
「CF316G3」Good Substrings
给出
个限制,每个限制包含 3 个参数 ,一个字符串满足当前限制当且仅当这个字符串在 中的出现次数在 之间。
现在给你一个字符串,问你 的所有本质不同的子串中有多少个满足所有限制。
将
注意到这个区间是由限制中的
于是两个二分确定
我们对每个限制做一个前缀和记录每个
回顾
「TJOI / HEOI2016」字符串
多次询问子串
的所有子串和 的最长公共前缀的长度的最大值。
注意到
考虑二分这个长度,就是要求:
!模型:固定
问题进一步:
这是个二维数点问题,考虑主席树维护即可。
「NOI2018」你的名字
求
中不在 中出现的本质不同的子串个数。
等价于求出现的个数。
考虑把所有
参考上一题的做法我们可以枚举
性质:当
, 最少变为 。(显然)
于是就不需要二分了,单 log 解决。
(也许以后还会做一些 sa 题,到时候可以补充进来)
总结
上面的例题可以总结为以下几类:
- 二分后缀排名
周末继续……
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具