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

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(r_1),suf(r_2),...,suf(r_n)}$


 

 

$Right:$

$Right(a)={r_1,r_2,...,r_n}$

一个状态的$Reg$由$Right$集合来决定

SAM中的一个状态s表示了一个$Right-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)\ \rightarrow\ Right(s) \subset 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
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 @ 2017-02-07 16:03  Candy?  阅读(11125)  评论(3编辑  收藏  举报