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


    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) ightarrow 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),但没关系,依旧看作一个点
     
  • 相关阅读:
    递归的形式过程
    c/c++程序员常见面试题分析(转)
    深入理解递归函数的调用过程(转)
    C语言union关键字
    中国象棋(java)
    求树中最低的公共祖先
    什么是Complement(补码)?
    LoadRunner,各协议之间的区别
    淘宝模板制作[店铺装修]学习过程+心得
    Tips of QTP
  • 原文地址:https://www.cnblogs.com/candy99/p/sam.html
Copyright © 2020-2023  润新知