[后缀自动机]【学习笔记】
SAM ..................Smith ?
参考资料:
1.陈立杰课件
3.https://huntzhan.org/suffix-automaton-tutorial/
4.http://codeforces.com/blog/entry/20861
说明:
花了晚上两个小时+一上午(估计还要一下午写笔记).....我主要看了clj的课件,算法过程主要依照课件上,其他两篇文章作为辅助补充一些证明和性质,课件中有点错误让我很纠结....
那些资料是非常好啦,然后我就乱写一点加深一下印象
前置技能:

有限状态自动机的功能是识别字符串,令一个自动机A,若它能识别字符串S,就记为A(S)=True,否则A(S)=False。
自动机由五个部分组成,alpha:字符集,state:状态集合,init:初始状态,end:结束状态集合,trans:状态转移函数。
不妨令trans(s,ch)表示当前状态是s,在读入字符ch之后,所到达的状态。
如果trans(s,ch)这个转移不存在,为了方便,不妨设其为null,同时null只能转移到null。
null表示不存在的状态。
同时令trans(s,str)表示当前状态是s,在读入字符串str之后,所到达的状态。
Suffix AutomatonSuffix Automaton
-------Direcged Acyclic Word Graph (DAWG)Direcged Acyclic Word Graph (DAWG)
Defination:
A suffix automaton A for a string s is a minimal finite automaton that recognizes the suffixes of s. This means that A is a directed acyclic graph with an initial node i, a set of terminal nodes, and the arrows between nodes are labeled by letters of s. To test whether w is a suffix of s it is enough to start from the initial node of i and follow the arrows corresponding to the letters of w. If we are able to do this for every letter of wand end up in a terminal node, w is a suffix of s. From:http://codeforces.com/blog/entry/20861
注意定义中的最简状态
Concept & Property:
ST(str)ST(str)表示trans(init,str)trans(init,str)
Reg(s)Reg(s)表示从状态ss开始能识别的stringstring,可以发现这是一些suffixsuffix的集合
(因为能识别str表明当前+str组成了一个suffix,那么str也是一个suffix)
对于string a
Reg(ST(a))=suf(r1),suf(r2),...,suf(rn)Reg(ST(a))=suf(r1),suf(r2),...,suf(rn)
Right:Right:
Right(a)=r1,r2,...,rnRight(a)=r1,r2,...,rn
一个状态的RegReg由RightRight集合来决定
SAM中的一个状态s表示了一个Right−equivalence classRight−equivalence class,也就是所有Right集合=Right(s)的子串
有了Right只需要一个长度就可以确定子串
对于Right(s),适合他的子串的长度在一个范围内,记作[Min(s),Max(s)]
对于任意两个状态a,b,Right(a)和Right(b)如果相交,设Max(a)<Min(b),那么Right(b)是Right(a)的真子集
//因为a是b的后缀啊,这里课件上写反了QAQ//
否则不相交
Right集合的包含关系形成的树形结构叫做Parent Tree
//其他文献中叫做Suffix Link 这个边是从孩子指向父亲的,和Fail Tree有点像//
Parent树从上往下Right集合变小,子串长度变长
fa=Parent(s) → Right(s)⊂Right(fa)且Right(fa)最小
发现Max(fa)=Min(s)−1
//Min(s)-1就多于Right(s)了,就到了Right(fa)//
Parent Tree的叶子节点数O(N),每个内部节点至少两个孩子,所以总结点数O(N)
//等比数列求和啊//
补充一点:
Max(s)也表示了SAM上root到s最多走几步,从root到s的所有路径范围就是[Min(s),Max(s)],因为一条路径就是一个能转移到s状态的子串啊
Suffix Link有点像失配吧,当前状态s走不了了就到Suffix Link指向的状态fa上去,fa是s的后缀所以是可行的,并且有更多走的机会
其他性质:
1.可以证明,在SAM中节点数不超过2n−2,边数不超过3n−3(包括转移边和Parent Tree的边)
2.Suffix Link从父亲指向儿子后就是Reverse(s)的Suffix Tree 反向字符串的后缀树!后缀树是一颗经过压缩的字典树
//随便证一下:一个节点Right等价,一段后缀相同(相当于压缩),然后Suffix Link连的点Right集合不同,也就是有不同的边出去了,所以不能压缩//
3.s有--c--> Parent(s)也有 并且人家Right还大
4.s--c-->t Max(t)>Max(s)
5.两个串的最长公共后缀,位于这两个串对应状态在Parent树上的最近公共祖先状态
子串的性质:
从init开始走转移边可以得到所有子串 每个子串都必然包含在SAM的某个状态里
一个状态的Right集合就是他的子树(叶子)的Right的并集
SAM Online Construction:
去看课件吧很详细啦......
使用了Parent Tree的性质,保证了空间O(N),时间也是O(N),证明不管啦
简单一说:
- 当前T,长度L,加入x
- 令p=ST(T) ,则Right(p)={L}
- 新建np=ST(Tx)
- p的Parent祖先的Right里都有L
- 对于没有--x-->的祖先v,trans(v,x)=np
- IF 一直到root之后也没有这样的祖先,Parent(np)=root
- ELSE p=第一个有的祖先,令q=trans(p,x) //注意这里的Right(q)={ri+1|s[ri]==c}不包括rn
- IF Max(q)==Max(p)+1,说明强行加入L+1不会使Max(q)变小,直接Parent(np)=q
- ELSE 新建nq复制q,用nq代替trans(v,x)=q的q, Parent(nq)=Parent(q),Parent(q,np)=nq
会使变小:Right(q)的Max可能更长一点,最后加上x还是没他长
Code:
实现上:
1.last保存当前的ST(T) , val保存MAX(s) 也就是到root的最远距离
2.和写过的其他数据结构不一样,root和last要新开节点,因为即使走到root还是可以走的,Parent(root)=0
3.好短啊 感觉比SA还好写
4.注意iniSAM和走之前u=root

int c[N],a[N]; int s[N]; struct State{ int ch[26],par,val; }t[N]; int sz,root,last; inline int nw(int _){t[++sz].val=_;return sz;} inline void iniSAM(){sz=0;root=last=nw(0);} void extend(int c){ int p=last,np=nw(t[p].val+1); for(;p&&!t[p].ch[c];p=t[p].par) t[p].ch[c]=np; if(!p) t[np].par=root; else{ int q=t[p].ch[c]; if(t[q].val==t[p].val+1) t[np].par=q; else{ int nq=nw(t[p].val+1); memcpy(t[nq].ch,t[q].ch,sizeof(t[q].ch)); t[nq].par=t[q].par; t[q].par=t[np].par=nq; for(;p&&t[p].ch[c]==q;p=t[p].par) t[p].ch[c]=nq; } } last=np; } void RadixSort(){ for(int i=0;i<=n;i++) c[i]=0; for(int i=1;i<=sz;i++) c[t[i].val]++; for(int i=1;i<=n;i++) c[i]+=c[i-1]; for(int i=sz;i>=1;i--) a[c[t[i].val]--]=i; } SAM

struct node{ int ch[26],par,val; }t[N]; int sz=1,root=1,last=1; void extend(int c){ int p=last,np=++sz; t[np].val=t[p].val+1; for(;p&&!t[p].ch[c];p=t[p].par) t[p].ch[c]=np; if(!p) t[np].par=root; else{ int q=t[p].ch[c]; if(t[q].val==t[p].val+1) t[np].par=q; else{ int nq=++sz; t[nq]=t[q];t[nq].val=t[p].val+1; t[q].par=t[np].par=nq; for(;p&&t[p].ch[c]==q;p=t[p].par) t[p].ch[c]=nq; } } last=np; }
Summary
一定要时刻把握这几条性质:
1.走 子串
2.Parent Tree的祖先 Right集合变大,字符串变短(路径长度变短),并且是后代的后缀哦
3.出现次数向父亲(Parent边)传递,接收串数从儿子(仍然Parent边)获取
4.拓扑排序/对val用基数排序 , 然后可以转移边/Parent边 DP ,可以倒着递推出|Right|
5.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,携手博客园推出1Panel与Halo联合会员
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· MySQL下200GB大表备份,利用传输表空间解决停服发版表备份问题
· 记一次 .NET某固高运动卡测试 卡慢分析
· 微服务架构学习与思考:微服务拆分的原则
· 记一次 .NET某云HIS系统 CPU爆高分析
· 如果单表数据量大,只能考虑分库分表吗?
· 7 个最近很火的开源项目「GitHub 热点速览」
· DeepSeekV3:写代码很强了
· 记一次 .NET某固高运动卡测试 卡慢分析
· Visual Studio 2022 v17.13新版发布:强化稳定性和安全,助力 .NET 开发提
· MySQL下200GB大表备份,利用传输表空间解决停服发版表备份问题