后缀自动机
sto 辰星凌 command_block orz
概念
后缀自动机是一种用于处理子串相关问题的字符串算法。
通常可以做到线性时间内处理一些复杂的子串问题。
可以和 线段树合并/LCT 等等的数据结构一起出。
有时候可以用 SA/后缀树 一类的东西代替。
概述
对于一个字符串 的子串 , 的 endpos 集合表示: 每次在 中出现时最后一个字符的下标构成的整数集。
约定 endpos 相同的子串为一个 endpos 等价类。
关于 endpos 集合有一些有用的性质:
-
两个子串的 endpos 集合要么交集为空,要么其中长串的 endpos 是短串 endpos 的子集。
-
如果两个子串的 endpos 相同,那么其中短串必定是长串的后缀。
-
对于一个 endpos 等价类,其中的子串长度构成一个连续的区间,并且其中长度为 的子串必定是长度为 的子串(如果有)的后缀。
-
endpos 等价类的个数为 级别。
事实上 endpos 集合的包含关系可以构成一棵树,称之为 parent tree。
注意 parent tree 的结点表示的是一个 endpos 等价类而非字符串。
记 表示 parent tree 中等价类 S 的父结点, 表示 endpos 等价类 S 中最短的字符串的长度, 表示 endpos 等价类 S 中最长的字符串的长度。
有性质 5:
存在构造方式使得 SAM 的结点和 parent tree 的结点相同,于是只需要考虑如何构造边集使得其满足 SAM 的性质:
-
父结点的 endpos 包含子结点的 endpos
-
父结点等价类中的所有子串都是当前等价类中任意子串的后缀
-
SAM 是一张 DAG
-
原串的所有子串都可以被 SAM 上的一条路径表示
事实上可以证明存在边数不超过 的构造方法。
构造
实际上 SAM 的构造和前面的推导没太大关系(笑
采用增量构造。
SAM 可以表示原串的所有子串,等价于可以表示每个前缀的任意后缀。因此可以考虑把每个长度的前缀的任意后缀都插入 SAM,实际上从 长度到 长度的增量就是若干 长度前缀的后缀加上第 个字符形成的新后缀。
对 parent tree 结点的定义:
其中 len 表示当前结点代表的等价类中最长字符串的长度。
假设要插入的字符为 c
,定义增量函数为:
对于 SAM 中的结点,其中存在一些 endpos 中含有已插入最长前缀长度的结点,不妨称这些结点为终止结点。
考虑在增量的同时维护变量 lst
表示上一次插入的字符所处的终止结点。
首先字符串总长增加 :
对于此前 SAM 中 np
的祖先结点,它们会代表一些长度为 的前缀的后缀。假设其中有一字符串 ,如果 不能被 SAM 中的结点表示,那么那么就从该结点到插入的结点连一条边,代表插入这个新后缀:
判断 p != 0
是防止跳出起始结点。
情况 1:如果没有其他结点可以表示产生的新后缀,根据定义有:
情况 2:存在其他结点可以表示产生的新后缀
如果这个结点的等价类里最长的串是新后缀:
那么这个结点的等价类里所有的字符串都是产生的新后缀,发现此时 ,于是:
反之,进入情况 3。此时 q 的等价类里有更长的串,因此它的 endpos 不包含 n,所以此时直接连边不满足 parent tree 的定义。
那么考虑将 q 拆分成两个结点,一个结点表示原等价类中长度在 的字符串,另一个表示原等价类中剩下的子串。注意此时两个结点表示的 endpos 不相等。
那么根据定义有 ,直接连边:
然后相应地把祖先结点的连边也一起修改:
具体正确性我不会证,之后再填坑吧。
采用这种方法构造的 SAM 点数不超过 ,边数不超过 ()
广义 SAM
普通 SAM 维护的是字符串,广义 SAM 维护的是 Trie。
也可以理解成是维护多个字符串。
网上有大量盗版广义 SAM,常见的是每次插入后直接令 lst = 1
或者用奇怪符号处理原串。这些在大部分情况下是对的,但是可以被卡。@辰星凌 的博客详细解释过这些问题。
定义增量函数为:
实际上正确的在线写法只需要添加两个特判:
- 如果该结点已经存在,则不用新建结点。
- 如果
nd[p].son[c]
已经存在且不为上面的情况,那么如果新建结点,则该结点为空(不保存任何信息)。此时也按照不新建结点的情况处理。
证明略,见参考博客。
于是得到广义 SAM 的增量函数。
注意插入新串时要重新将 lst
设为
如果要对多个字符串维护信息,需要对每个字符串分别存储信息,例如求出现次数不能共用一个变量而是要开数组。
用结构体实现可能会被 卡常,写数组会快很多,但数组写法先咕了
套路
子串出现次数
结论:SAM 中结点的出现次数 等价于 子树大小。
考虑 parent tree,对于子结点出现过的位置,其父结点(后缀)一定也出现过。因此每次插入字符时将其出现次数设为 ,然后在 parent tree 上求子树和即可。
本质不同子串个数
-
做法一:等价于求
做法二:注意到 SAM 是 DAG,所以等价于路径计数。
-
P4070 [SDOI2016]生成魔咒
动态求不同子串个数。
每次新建 SAM 结点的时候加入其贡献就行,注意
nq
没有贡献。
最长公共子串
SP1811 LCS - Longest Common Substring
考虑字符串所有的子串都可以表示成:原串某个前缀的后缀。
于是考虑对其中一个串建立 SAM,在另一个串上跑匹配。
枚举匹配串的前缀,如果 SAM 中有当前字符的转移则转移,反之一直向上跳,直到存在转移为止。得到的是此前缀的某一个后缀,也就是这个前缀和另一个串的 LCS.
第 k 小子串
跑一遍路径计数,然后类似于平衡树地递归求即可。
求等价类的 endpos
考虑线段树合并。
考虑遍历原串的每一个前缀所在的等价类,将这些结点作为初始条件,暂时令其 endpos 为自身长度。
然后将所有等价类按照长度排序,然后倒序线段树合并。
具体的以后再补,咕咕咕。
parent tree 相关
多串匹配
SP8093 JZPGYZ - Sevenk Love Oimaster
考虑对所有模板串建立广义 SAM,插入每个前缀时在相应结点打上标记。
查询时找到该串对应的 SAM 结点,转化成在该结点的子树内数不同的标记数量。线段树合并维护。
动态维护出现次数
P5212 SubString
每次给定一个字符串,将其 接在已有串的末尾 或者 询问其在已有串的出现次数。
考虑到某个串的出现次数可以转化成 parent tree 上的子树和,于是考虑转化成动态加点,维护子树和。
注意到增加一个结点的贡献等价于:其到根的路径上每个结点的子树和加一。用 LCT 维护动态链加,单点查权值就行。
parent tree 上倍增
CF666E Forensic Examination
类似求 LCS,考虑维护 的每个前缀 和 匹配的最长长度 和此时对应的 SAM 结点 。
对于询问 ,考虑从 向上倍增,直到找到等价类中包含长度 的结点。
此时的问题是该结点对应的字符串在 中出现次数最多的串。
考虑用线段树合并维护,插入时对于前缀结点 ,令其对应的字符串编号为 ,在线段树上 处加一。
后缀树
反串的 parent tree.
性质: 等于 在后缀树上 LCA 的深度.
资料
史上最通俗的后缀自动机详解 - KesdiaelKen 的博客 - 洛谷博客
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!