\(\rm SAM:\ Suffix\ Automaton\) ,后缀自动机。
远古博客,有许多纰漏,慎重食用!
难理解,但代码好写(当初敲完模板题没总结,现在忘光了QAQ)
总体上是 后缀 trie + parent tree ,构成一个可以表示所有子串的 DAG
福利:后缀自动机可视化
推荐博客:
- KesdiaelKen 的博客(我就是看这博客学会的)
- shadowice1984 的博客
- xzyxzy 的博客
这博客主要是我总结自己不好理解的地方,不是很全面
声明:本文中图片大部分为 BANANA 从他人博客
盗来的借鉴的,一些语言表达借鉴于 KesdiaelKen 的博客 ,若有侵权请 联系BANANA 删除
思路
前置
先了解几个概念:
后缀 trie :把字符串的每一个后缀插入到 trie 树里
一个普通的后缀 trie ,有很大部分节点是可以压缩的
endpos 集合 :一个子串在原串出现的位置(可能出现多次)的右端点集合
比如一个串“banana”
\(endpos(b)=\{1\},\ endpos(a)=\{2,4,6\}\)
\(endpos(an)=\{3,5\},\ endpos(ana)=\{4,6\}\)
parent tree :一个子串前面添加一个或几个字符,可以将他的 endpos 集合分割,endpos 集合之间便有了父子关系
parent tree 上每一个节点表示的 endpos 集合唯一地表示一个后缀集合
不好举例,自己手模吧
构造
将后缀 trie 和 parent tree 结合起来便是 SAM 的 DAG
那么一条路径表示原串的一个子串,一个节点 x 表示根节点到 x 路径形成的所有串的 endpos 集合
SAM 常见的增量构造:将字符串从前到后一个一个字符插进去(离线构造窝不会)
先确定一些量:
\(fa[x]\) :x 节点表示的集合在 parent tree 上的父亲
\(len[x]\) :x 节点表示的集合中最长的串的长度
如果设最短的串长为 \(minlen[x]\) ,那么 \(len[fa[x]]+1=minlen[x]\) ,即 parent tree 上的分割关系
我们考虑从后面插入一个字符 c,会有什么变化:
- 多出新串的后缀
- 新串后缀的 endpos 集合改变
那么在 trie 的意义上要把旧串所有后缀的后面加上字符 c
在 SAM 上实现就是先新建节点 np
然后把最后一个节点 p、它在 parent tree 的父亲以及祖先的 c 儿子设为该节点(相当于压缩地遍历旧串的所有后缀)
并且 \(len[np]=len[p]+1\)
1 | int p = last, np = last = ++cnt; |
如果这些祖先都没有 c 儿子,到达最高的祖先就是根节点
说明字符 c 是一个新字符,parent tree 上根节点多一个儿子:只有位置 c 的 endpos 集合
1 | if (!p) fa[np] = 1; |
那么如果有一个祖先 p 已经有了 c 儿子,设它为 q
说明旧串有子串的结尾是 c 字符,那么就要看这个 q 的 len 了
\(len[q]=len[p]+1\)
说明 q 在插入时 p 就是最后一个节点了
那么根到 q 点表示的子串都是根到 np 表示的子串的后缀
在 parent tree 上,q 是 np 的父亲
\(len[q]>len[p]+1\)
在 parent tree 上,q 和 np 是兄弟
那么就新建一个 nq (复制一份 q )作为它俩的父亲
将之前 p 及其祖先指向 q 的 c 儿子全指向 nq 即可
1
2
3
4
5
6
7
8
9
10
11int q = ch[p][c];
if (len[q] == len[p] + 1)
fa[np] = q;
else {
int nq = ++cnt;
memcpy(ch[nq], ch[q], sizeof ch[q]);
len[nq] = len[p] + 1;
fa[nq] = fa[q], fa[q] = fa[np] = nq;
while (p && ch[p][c] == q)
ch[p][c] = nq, p = fa[p];
}
code
完整构造代码,看起来一堆 while
却是\(O(n)\)。
注意点数是 \(2n\) 规模。
1 | struct Suffix_Automaton { |
应用
判断子串
直接在 SAM 上跑,跑完没到 NULL 即为子串
求不同子串个数
设 \(f[i]\) 为从 i 点出发的子串, \(f[i]=\sum_{(i,j)\in Edge}{f[j]+1}\)
好像还可以直接求 \(\sum{(len[i]-len[fa[i]])}\) ,因为 SAM上无重复子串
待更~
题目
BANANA 由于过菜,还不会写几道题~
求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值
一个子串的出现次数即在 parent tree 上该子树的 size
暴力建出 parent tree,或者拓扑一下 DAG ,统计
每次向后插入一个字符,并求每次操作后不同子串个数
显然 SAM 应用在求不同子串个数 \(\sum{(len[i]-len[fa[i]])}\)
每次只增加一个 len 影响总个数的节点,ans += len[x] - len[fa[x]]
,于是每次 \(O(1)\) 统计答案
大坑:“字符”范围 1e9 ,每个节点要用 map 存儿子
- 待更~