后缀数组学习笔记
定义
后缀数组是什么?
(下文用
后缀数组包含两个数组
表示后缀 排序后的排名。 表示排名为 的后缀的编号。
显然有
求法
倍增 。
对于第
特别地,对于第
用 sort
可以做到
rep(i, 1, n) sa[i] = i, rk[i] = s[i];
for (int len = 1; len < n; len <<= 1) {
auto cmp = [&](auto x, auto y) {return rk[x] == rk[y] ? rk[x + len] < rk[y + len] : rk[x] < rk[y];};
sort(sa + 1, sa + 1 + n, cmp);
int p = 0;
rep(i, 1, n) {
if (!cmp(sa[i], sa[i - 1]) && !cmp(sa[i - 1], sa[i])) o_rk[sa[i]] = p;
else o_rk[sa[i]] = ++p;
}
copy(o_rk + 1, o_rk + 1 + n, rk + 1);
}
rep(i, 1, n) cout << sa[i] << ' ';
基数排序优化
先扯扯基数排序是什么。
基数排序
考虑比较两个字符串的大小,可以将一个字符串拆成
- 首先若
和 的长度不相同,那么在长度短的字符串后面补上若干个 。接着进入第一个关键字的比较。 - 若
:若 则 更小,否则 更小。否则 ,进入下一个关键字的比较。 - 若
:若 则 更小,否则 更小。否则 ,进入下一个关键字的比较。
- 如果到最后都没有分出大小,则
。
考虑把这种方式用在整数比较上。把整数拆成若干个关键字的集合,可以把整数的每
如果是对序列
const int M = 1E5;
copy(a + 1, a + 1 + n, b + 1);
rep(i, 1, n) ++cnt[b[i] % M];
re(i, 1, M) cnt[i] += cnt[i - 1];
per(i, n, 1) a[cnt[b[i] % M]--] = b[i];
copy(a + 1, a + 1 + n, b + 1);
fill(cnt, cnt + M, 0);
rep(i, 1, n) ++cnt[b[i] / M];
re(i, 1, M) cnt[i] += cnt[i - 1];
per(i, n, 1) a[cnt[b[i] / M]--] = b[i];
在这里基数排序可以替代 sort
,因为基数排序本来就是处理多关键字排序的利器,这里恰巧是双关键字排序。
复杂度变为
rep(i, 1, n) sa[i] = i, rk[i] = s[i], ++cnt[rk[i]];
int m = 127;
rep(i, 1, m) cnt[i] += cnt[i - 1];
per(i, n, 1) sa[cnt[rk[i]]--] = i;
int p = 0;
rep(i, 1, n) {
if (rk[sa[i]] == rk[sa[i - 1]]) o_rk[sa[i]] = p;
else o_rk[sa[i]] = ++p;
} copy(o_rk + 1, o_rk + 1 + n, rk + 1);
for (int len = 1; len < n; len <<= 1, m = n) {
fill(cnt + 1, cnt + 1 + m, 0);
copy(sa + 1, sa + 1 + n, id + 1);
rep(i, 1, n) ++cnt[rk[id[i] + len]];
rep(i, 1, m) cnt[i] += cnt[i - 1];
per(i, n, 1) sa[cnt[rk[id[i] + len]]--] = id[i];
copy(sa + 1, sa + 1 + n, id + 1);
fill(cnt + 1, cnt + 1 + m, 0);
rep(i, 1, n) ++cnt[rk[id[i]]];
rep(i, 1, m) cnt[i] += cnt[i - 1];
per(i, n, 1) sa[cnt[rk[id[i]]]--] = id[i];
int p = 0;
rep(i, 1, n) {
if (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + len] == rk[sa[i - 1] + len]) o_rk[sa[i]] = p;
else o_rk[sa[i]] = ++p;
} copy(o_rk + 1, o_rk + 1 + n, rk + 1);
}
rep(i, 1, n) cout << sa[i] << ' ';
应用
- P4051 字符串加密
将
- 判断字符串中是否出现某一子串
假设要在
- 「USACO07DEC」Best Cow Line
处理出来原串的后缀数组和其反串的后缀数组,每次取时比较一下两个
数组
定义
假设
求法
引理:
证明:当
时式子显然成立;当 时:
根据定义有。
假设用来表示最长公共前缀,则 表示为 , 表示为 。( , , 可能是空串, 不是空串)
可以得出, 。( )
又且不存在后缀 满足 故得到
,所以 与 至少有公共前缀 。
即又因为 ,得到 ,证毕。
根据这个结论,就可以像 KMP 那样求
code:
int k = 0;
rep(i, 1, n) {
if (k) --k;
while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
height[rk[i]] = k;
}
应用
根据子串是后缀的前缀这个性质,
- 两个子串的最长公共前缀
有定理
基于这个定理,求子串
- 比较一个字符串的两子串大小
假设需要比较
-
若
,那么 。(其实这部分也可以用哈希做) -
否则
。 -
本质不同子串数量
首先子串是后缀的前缀,所以大体是枚举后缀,再看有多少个后缀的前缀重复了。
考虑按照后缀排序的顺序来计算重复的前缀数量,因为有
故总计重复的前缀数就是
- 是否有某字符串在文本串中至少不重叠地出现了两次
可以二分目标串的长度
- 最长回文子串
求一个字符串中的最长回文子串。
可以用哈希做,枚举回文中心后二分并判断反串是否等于正串即可,
这里还有一个 SA 的做法。设当前字符串为 |
分割,记新串为
- 重复次数最多的连续重复子串
一个字符串为连续重复子串当前仅当存在一个字符串重复若干次后得到该串。求一个字符串中重复次数最多的连续重复子串。
这种问题的基本方法是枚举长度并处理关键点。
考虑枚举重复子串的长度
- 最长公共子串
求两个字符串的最长公共子串。
这种两个字符串的方法是将一个放在另一个后面并用分隔符隔开,接着对新串用后缀数组。
假设是字符串
- 不可重最长重复子串
求一个字符串中最长的重复了两遍的子串,且出现的位置不能重叠。
主要思想是对
先二分答案,题目转化成:判定是否存在两个长度为
所在组编号 | 后缀 | ||
---|---|---|---|
这样分组后,会有几个性质:
- 每组的字符串两两之间的
长度 。 - 对于同一组,字符串两两之间的
的长度为 的前缀都相同。 - 对于两个不同的组,字符串的
对应的长度为 的前缀不同。
根据这些性质,对于某组,只需判定是否存在两个在这组的不相交的子串即可。
- 出现在至少
个字符串中的最长子串
有
个字符串,查找出现在至少 个字符串中的最长子串。
结合 最长公共子串 和 不可重最长重复子串 的方法,先将这
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通