SAM 学习笔记
前言
老年选手发现自己还不会后缀自动机。
老年选手觉得这非常离谱。
老年选手决定学一下。
正片
1
考虑怎么在一个 DAG 上表示出一个字符串的所有子串。
最简单的方法就是建一个 trie,把它的每个后缀扔进去。
那么它有什么性质呢?
- 有一个源点,若干个终止点。边代表一个字符。从源点到任意一个节点的任意路径可以形成一个原串的子串。从源点到任意节点的任意路径不能形成的字符串均不是原串子串。简单来讲,这个图可以表示且仅可以表示原串的所有子串。
- 从源点到任意终止节点的路径均为原串后缀。
- 从源点出发的任意两条不同路径形成的字符串不相同。
但是,如果直接建 trie,节点数是 的。事实上,图中很多节点都可以合并,比如下图,两个黄白黄的子树就可以合并。
现在要解决的问题是,构造一个节点数,边数尽量少的 DAG 满足上面三个条件。
2 后缀自动机的一些性质
一些定义:
对于一个子串,它在原串中可能出现在若干的位置。而一个子串 出现的这些位置的右端点标号组成的集合,我们称之为 。如下图,当原串为 时, 。
下面有几个结论,比较重要。
2.1 如果两个子串的 相同,则其中一个子串必然为另一个的后缀
2.2 对于任意两个子串 和 ,要么 ,要么
2.3 对于 相同的子串,我们把它们归为一个 等价类。对于任意一个 等价类,他们中的字符串长度连续,并且短的字符串是长的字符串的后缀
2.4 等价类个数的级别为
对于一个类,其中有最长的一个子串 。在 第一个字符前增加一个字符得到 ,使得 是原串子串。那么 与 并不在一个等价类内,且有 。可以看作是对 集合的一个分割,最终形成的集合个数不会超过
所以类之间就有了父子关系。
我们称上图这棵树为
2.5 在一个类 中,我们称最长子串的长度为 ,最短子串的长度为 。对于后缀树上存在父子关系的两个类,则
在一个类的最长子串前再添加一个字符,形成的字符串属于其儿子中的一类,且是它所属的类中最短的一个。
因此,我们只需要保存 和 即可, 可由其父亲推出
我们定义 表示 中最长子串, 表示 中最短子串
把每一个类的最长子串写在节点旁长下面这样,原串是
3 构造
后缀自动机通过单个地增加字符实现构造
给出代码
struct __{int ch[26],fa,len;}t[N];
int lst=1,cnt=1;
inline void insert(char c)
{
int p=lst,node=++cnt; lst=node;
t[node].len=t[p].len+1;
for (;p&&!t[p].ch[c];p=t[p].fa) t[p].ch[c]=node;
if (!p) t[node].fa=1;
else
{
int q=t[p].ch[c];
if (t[q].len==t[p].len+1) t[node].fa=q;
else
{
int clone=++cnt;
rep(i,0,25) t[clone].ch[i]=t[q].ch[i];
t[clone].fa=t[q].fa;
t[clone].len=t[p].len+1;
t[q].fa=clone;
for (;p&&t[p].ch[c]==q;p=t[p].fa) t[p].ch[c]=clone;
t[node].fa=clone;
}
}
}
结构体的 和前面的定义一致, 表示 上的父亲, 和 里的边意义相近。
是此前最长的前缀所属的节点的编号。
注意初始时,,因为我们手动加入了一个空串。
记 代表的串为 , 代表的串为 。
那么显然, 是 加一个字符 得到,同样地, 的后缀也可以通过 的后缀加一个字符 得到,那么 for 一下 在 上的祖先。这一步可以理解为压缩地遍历一个串地所有后缀。
for (;p&&!t[p].ch[c];p=t[p].fa) t[p].ch[c]=node;
对于节点 ,如果它没有 边,即 并非旧串子串,那么就建一条新边到 。此时 等于 ,所以它们是一个等价类。如果它有 边,此时 已经在旧串中出现过,那它的 不等于 ,就不合法,。
if (!p) t[node].fa=1;
如果已经遍历完了旧串所有的后缀,且它们加 都不是旧串地子串,说明不可能存在除节点 1 以外的祖先(因为也会遍历空串)
else
{
int q=t[p].ch[c];
if (t[q].len==t[p].len+1) t[node].fa=q;
else
{
int clone=++cnt;
rep(i,0,25) t[clone].ch[i]=t[q].ch[i];
t[clone].fa=t[q].fa;
t[clone].len=t[p].len+1;
for (;p&&t[p].ch[c]==q;p=t[p].fa) t[p].ch[c]=clone;
t[q].fa=t[node].fa=clone;
}
}
但是,如果 在第一个有 边的祖先停下了,记 通过 边连向 。
这时, 内的子串是 的后缀。所以 是 在后缀树上的祖先。
注意到,后缀树上的节点,满足 。
那么,此时有两种情况。
第一种:
因为 是新串后缀,又 ,所以 是新串后缀。又因为 内的所有串都是 的子串,所以它们的 集合整体并上了 ,它们的 集合还是相同的,直接把 的 置为 即可
第二种:
那么在加入新点之后, 里长度 的串的 集合并上了 。长度 的串的 集合不变。所以 就不再符合等价类的定义了。这时要把 分裂,变成 和 ,其中 包含原 中长度 的子串, 中包含原 中长度 的子串。
那么考虑 的信息。首先 。对于 的字母出边,和 是一样的。因为原 中的所有子串都是新串的后缀,在它们之后加字符, 集合不会改变。
然后这时,,因为它满足 ,并且 。
这时,再考虑 在后缀树上的祖先。如果它的 边指向原 ,因为 ,所以对于 的祖先的每一个子串,它的 集合会比之前并上 。所以要把它们的 边指向 。如果它的 边不指向原 ,那么它会指向原 后缀树上的一个祖先。它们的祖先的 集合都整体地并上了 ,不用更改。
最后,我们再考虑 的信息。它的后缀树上的父亲应该是 ,因为 中含有 ,而 中不含。
4 应用
这我咋知道,我又没做过题(
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!