[后缀自动机]【学习笔记】

SAM ..................Smith ?


参考资料:

1.陈立杰课件 

2.一篇经典俄文的翻译

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之后,所到达的状态。
DFA简介
复制代码


Suffix Automaton

-------Direcged Acyclic Word Graph (DAWG)

Defination:

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)表示trans(init,str)

Reg(s)表示从状态s开始能识别的string,可以发现这是一些suffix的集合

(因为能识别str表明当前+str组成了一个suffix,那么str也是一个suffix)

对于string a

 Reg(ST(a))=suf(r1),suf(r2),...,suf(rn)


 

 

Right:

Right(a)=r1,r2,...,rn

一个状态的RegRight集合来决定

SAM中的一个状态s表示了一个Rightequivalence class,也就是所有Right=Right(s)的子串

有了Right只需要一个长度就可以确定子串

对于Right(s),适合他的子串的长度在一个范围内,记作[Min(s),Max(s)]

对于任意两个状态a,bRight(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中节点数不超过2n2,边数不超过3n3(包括转移边和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
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;
}
Another SAM
复制代码

 

 



Summary

一定要时刻把握这几条性质:

1.走 子串

2.Parent Tree的祖先 Right集合变大,字符串变短(路径长度变短),并且是后代的后缀哦

3.出现次数向父亲(Parent边)传递,接收串数从儿子(仍然Parent边)获取

4.拓扑排序/对val用基数排序 , 然后可以转移边/Parent边 DP ,可以倒着递推出|Right|

5.

 


 


 

Generalized SAM

研究了两节多课广义后缀自动机是什么,还看了2015国家队论文,然后发现,广义后缀自动机不就是把很多串的SAM建到了一个SAM上,建每个串的时候都从root开始(last=root)就行了........
广义后缀自动机是Trie树的后缀自动机,可以解决多主串问题
这样的在线构造算法复杂度为O(G(T)),G(T)为Trie树上所有叶子节点深度和,发现G(T)<=所有主串总长度
还有一种离线算法,复杂度O(|T||A|) ,不学了吧
 
一个基本应用是求出每个状态出现次数(同一个串算一次)
根据接收串数从儿子获取,就是子树中有多少主串经过
方法是:
先建出SAM
然后跑每个主串,状态维护cou和cur分别为出现次数及上一次出现是哪个串
出现次数向父亲传递,所以要沿着Parent向上跑更新,遇到cur=当前串的就不用继续跑了
如果题目规定串总长L,N个串
这样最坏情况下复杂度为O(L^3/2),发生在N=每个串长度的时候(均值不等式啊)
 
 
对Trie建广义后缀自动机:
从根dfs中保存last
解释
直接对Trie建SAM与原本一个串建SAM唯一的不同是last可能已经有--c-->q了,我们有两种选择来处理
如果t[q].val==t[last].val+1
第一种是直接不管,这样t[np].par=q,np和q可以看作一个点,不受影响
第二种是管,直接让last走到q,也没关系
如果t[q].val!=t[last].val+1
这时会新建节点nq,然后t[q].par=t[np].par=nq 注意np和nq的Right是一样的(因为本来last有--c-->这个转移啊,所有的r都可以r+1),但没关系,依旧看作一个点
 
posted @   Candy?  阅读(11133)  评论(3编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示