基础后缀数据结构简记
约定 是字符串长度 . 是 的 LCP 的长度 .
后缀数组
后缀排序
问题:将字符串 的所有后缀排序 .
核心思想:倍增考虑,把从每个位置子串拆成两个减半长度的子串的双关键字排序 .
使用基数排序即可 ,这里 和字符集同阶 .
令 是第 小的后缀的开头位置, 是 的排名 .
Code
有一些常数优化的技巧可以看 OI-Wiki .
const int m = 1000000;
auto comp = [&](int i, int j, int k){return (y[i] == y[j]) && (y[i+k] == y[j+k]);};
for (int i=1; i<=n; i++) ++buc[x[i] = a[i]];
for (int i=1; i<=m; i++) buc[i] += buc[i-1];
for (int i=n; i>=1; i--) sa[buc[x[i]]--] = i;
for (int k=1; k<=n; k<<=1)
{
int p = 0;
for (int i=n-k+1; i<=n; i++) y[++p] = i;
for (int i=1; i<=n; i++)
if (sa[i] > k) y[++p] = sa[i] - k;
memset(buc, 0, sizeof buc);
for (int i=1; i<=n; i++) ++buc[x[y[i]]];
for (int i=1; i<=m; i++) buc[i] += buc[i-1];
for (int i=n; i>=1; i--) sa[buc[x[y[i]]]--] = y[i];
swap(x, y);
x[sa[1]] = p = 1;
for (int i=2; i<=n; i++) x[sa[i]] = comp(sa[i], sa[i-1], k) ? p : ++p;
if (p >= n) break;
m = p;
}
for (int i=1; i<=n; i++) rk[sa[i]] = i;
height 数组
问题:求解 .
核心思想:,从而暴力做就是 的 .
Code
for (int i=1, j=0; i<=n; i++)
{
if (j) --j;
while (a[i+j] == a[sa[rk[i]-1]+j]) ++j;
h[rk[i]] = j;
}
常见应用
LCP:变成区间 height 数组的最小值 .
比大小:求出 LCP 后就好处理了 .
本质不同子串:考虑计数重复子串,仔细考察每次加一个子串肯定新增恰 个子串,那么就做好了 .
我也没做啥题,就写这三个吧 .
后缀自动机
结构
我们期望得到一个 DFA,恰接受某个字符串 的每个子串 .
对于一个子串 ,定义 为 中所有 的结束位置所构成的集合 .
字符串的所有子串可以由 endpos 分为若干个等价类,下面称 等价当且仅当 .
考虑由此构建一个自动机,其每个状态对应一种 endpos,这样的自动机就被称为 的后缀自动机,转移就是表示每次加一个字符的影响,这里转移构成的 DAG 也被称作 DAWG . 最小性可以由 Myhill–Nerode 定理导出 .
断言:
- 满足 的子串 等价当且仅当 在 中的所有出现都作为 的后缀 .
- 对于子串 ,当 是 的后缀时 否则 .
- 对于一个等价类其中包含的子串长度肯定是连续的 .
其实仔细想想都是显然的,这里就不详细说明了 .
关于第三个性质,后令 分别表示一种 endpos 对应的最长子串长度和最短子串长度 .
最好类似 AC 自动机,得到一个 fail 树状物,对于等价类 ,考察其所有后缀,记 为和 不在一个等价类里的的最长后缀, 是对应等价类,则连边 . 由此定义后缀链接 .
令根 满足 ,则断言 关系组成一棵树(通常称为 parent 树).
这是相对显然的,如果你注意到 那么问题就不难了 .
parent 树所得到的树也可以表达 endpos 的包含关系,首先 endpos 的包含肯定组成树,那么只需要说明他们等价即可,问题也就是说明 ,根据前面的结论这是显然的 .
从而,后缀自动机的基本结构已经被描摹完成 .
构建
这里采用增量构建,也就是说考虑每次加一个字符 产生的影响(这也表明这个算法是在线的).
假设加入前表示整个字符串的结点是 ,加入后是 ,核心问题就是确定 . 不断跳 的 link 指针直到跳没了或者其存在一条走 的转移边 . 记最终得到的点是 ,转到 ,考察 这个位置的信息 .
如果跳没了那么肯定是连 就完了,否则讨论:
- ,这时候相当于直接继承原来等价类,连 即可 .
- ,这时加入 会导致原等价类分裂,所以需要把 拆成两份,一份作为原来的等价类,另一份作为 . 此时需要把指向 的结点全部改成指向 的 .
仔细地进行分析即可得到加入 个字符的时间复杂度为 (如果字符集较大需要使用 Hash,具体见生成魔咒).
(具体复杂度分析看 OI-Wiki 吧)
详细算法流程:
Code
注意开二倍空间 .
// len : maxl
struct SAM
{
int t[N][S], link[N], len[N], lst, cc;
SAM() : lst(1), cc(1){}
inline void extend(int c)
{
int p = lst, now = lst = ++cc;
len[now] = len[p] + 1;
while (p && !t[p][c]){t[p][c] = now; p = link[p];}
if (!p){link[now] = 1; return ;}
int q = t[p][c];
if (len[q] == len[p] + 1){link[now] = q; return ;}
int k = ++cc;
memcpy(t[k], t[q], sizeof t[q]);
len[k] = len[p] + 1; link[k] = link[q]; link[now] = link[q] = k;
while (p && (t[p][c] == q)){t[p][c] = k; p = link[p];}
}
};
应用
作为练习可以做一下弦论那题,很简单的(突然想起来 DAG 第 大路径是 CSP 初赛题).
求子串出现次数:也就是要求 endpos 集合的大小,子串也就是每个前缀的所有后缀,也就相当于把每个前缀对应的结点在 parent 树的根链上每个点贡献都加一,DFS 一遍即可 .
求本质不同子串数:每个等价类 对应的子串个数就是 ,全部加起来即可 .
求最长公共子串:考虑对一个字符串建 SAM,另一个字符串在上面跳,每次能转移就转移否则跳 link,对于所有位置取 max 即可(详见熟悉的文章).
后缀 LCP:对于 , 就是 对应前缀的最长公共后缀(其实算后缀树的结论).
广义后缀自动机
相当于对多个字符串的所有子串建立的后缀自动机 .
同样考虑每次加一个字符 . 对于每个串分别处理一个 依次加入,只有一种情况是额外的,也就是 途径 的转移边存在的情况,设其到达点 .
讨论是类似的:
- ,说明根本不用加,最后让 即可 .
- ,类似地分裂 即可 . 这里新的 应该是分裂出去的结点 .
那么就完了 . 代码基本是一样的我就不放了 .
后缀树
后缀树的东西我也不是很懂,摆了(
后缀树
问题:求出字符串 所有后缀组成的(压缩)Trie .
这里压缩指压缩一条没有向外的边的链,因为只有 个叶子所以这里的结点数是 的 .
构建
其实也就是说要求所有叶子的虚树,考虑那个单调栈维护右链的构建方法,通过 SA 就天然维护了 .
或者也可以通过 SAM 构建,因为反串的 parent 树就是后缀树 .
以下是博客签名,正文无关
本文来自博客园,作者:yspm,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/17865778.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】