后缀自动机 (SAM) 学习笔记
定义
后缀自动机(,简称 )是一种用于字符串处理的有限状态自动机(),它根据母串的所有后缀构建,能识别出母串的所有子串,且构造算法时间复杂度为线性 ,空间复杂度为 , 是字符集大小,这里将 看作常数。
的性质
基础性质
-
从后缀自动机的源点开始走到任意节点的路径都对应了母串的一个子串。
- 考虑母串的一个后缀 ,该后缀可以由源点走出,则在走出路径的任意位置截断作为新的路径,都可以对应该后缀的某个前缀,即 。
-
母串的任一子串都与从源点出发的一条路径对应,且该对应关系是唯一的。即路径不同,形成的子串不同;子串不同,路径不同。
-
后缀自动机的点数与边数都为线性。
边的种类
后缀自动机上有两种边:
- 转移边,和 的构建方式一样,在某个节点表示的所有字符串的结尾加上一个字符连出的边。
- 链,表示后缀之间的包含关系,连向该点表示的最短的字符串删去首字母后所有后缀对应的节点状态。这些边可以组成一棵 。
定义
一个子串在母串中出现的位置的右端点形成的集合。
例如母串 ,子串 ,则 ,即 。
则可以得到,一个 集合可能对应多个子串。
中的一个节点的状态与一个 集合相互对应,所以不存在两个不同节点的状态对应同一个 集合,每个节点对应的 集合互不相同。
性质
记 表示子串 的 集合, 表示字符串 的长度。
-
若 且 ,则 为 的后缀。
- 对于任意一个右端点位置 , 也出现在该位置,且长度不大于 ,根据 ,可以得到 为 的后缀。
-
对于两个不同子串 ,设 ,要么 ,要么 。根据 的关系也可以反推 与 的关系。
- 要么 是 的后缀,此时 出现的位置 一定出现,但 出现的位置 不一定出现,要么 与 无关。
-
一个 集合对应多个子串,假设所有这些不同子串是 且满足 ,此时一定满足 , 是 的后缀。
- 例如 且满足 ,可以得到 以及 。考虑 出现的位置集合与 出现的位置集合相同以及 是 的后缀,则 所有长度为 的后缀也一定在这些位置集合出现。
根据定义比较难理解,可以考虑下图,母串 。
性质
记 表示子串 出现但以 为后缀的子串 都不出现的位置集合, 表示节点 的 集合, 表示节点 的最短子串, 表示节点 的最长子串, 表示节点 的子节点构成的点集。
-
。
- 根据定义,节点 的最短子串删除首字母即得到了节点 的最长子串,每个节点恰好代表若干长度连续的后缀。
-
。
- 显而易见节点 的 集合为子节点 的 集合的父集,且包含节点 自己的子串出现的位置,也就是这些集合的并。
根据这两个性质可以分析出沿着 链向上跳本质上就是不断从后缀中删去前缀的过程,也可以分析出点数最多有 个(考虑多个子节点的 集合大小越接近时点数越多,也就是等比数列求和),进一步得到边数最多为 条( 个点的生成树占据 条,母串最多有 个不同的后缀,从源点走到不能再走的点代表一种后缀,最多 条,加起来最多 条)。
构建后缀自动机
运用增量法构造,假如已经构建完母串的前缀 的后缀自动机,在此基础上增加第 个字符 形成新的后缀自动机。
加入第 个字符时一共有 种情况(下面举例时假设已经构建好 ab
的后缀自动机):
-
不论哪种情况,首先将对应 的节点连出一条新边,边上字符为 ,也就是在末端加入一个新字符。
-
不断在 上向上跳,直到存在一个和 一样的出边。
-
abc
- 加入字符c
时,跳到了源点都没有字符c
的出边,此时将跳的时候经过的点连一条字符为c
的边向新点即可,在 上将新点父亲设为源点。 -
aba
- 加入字符a
时,跳到了源点才发现有字符a
的出边,记跳到源点前的一个点为 ,源点连出字符a
的出边到达的点为 ,则 是连向新点的最短子串的点,满足 ,判断 的最长子串是否由源点转移而来,此种情况下是,所以 。且源点在 上是 的父亲,所以满足 。推出 ,所以将 在 上的父亲设为 即可。 -
abb
- 加入字符b
时,跳到了源点才发现有字符b
的出边,但此时 的最长子串不由源点转移,而是由另一点 (这个例子中 是 )转移 ,此时 一部分由源点转移,一部分由 转移,将 分裂为 和 ,各自都保留 的出边,其中 是由源点转移而来的, 是由 转移而来的,显然 为 的后缀,于是将 在 上的父亲设为 ,再将 在 上的父亲也设为 即可。
该例子不仅包含了该后缀自动机的构建,也包含了总体的三种情况,实现时按照三种情况分类讨论即可。
代码实现
struct Node{
int son[26],len,fa;
Node(){ memset(son,0,sizeof son); len=fa=0; }
} node[N<<1];
int last=1,tot=1; //last 表示前缀 S[1~n] 的节点,tot 表示 SAM 的总节点数
inline void add(int c){
int p=last,nw=last=++tot; node[nw].len=node[p].len+1; //新建一个节点
for (; p&&!node[p].son[c]; p=node[p].fa) node[p].son[c]=nw; //跳父链将节点的出边连向新节点
if (!p) node[nw].fa=1; //情况1: 源点
else {
int q=node[p].son[c];
if (node[q].len==node[p].len+1) node[nw].fa=q; //情况2: p -> q
else {
int xq=++tot; //情况3: t -> q
node[xq]=node[q]; node[xq].len=node[p].len+1; node[q].fa=node[nw].fa=xq;
for (; p&&node[p].son[c]==q; p=node[p].fa) node[p].son[c]=xq;
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】