后缀三兄弟之三——后缀自动机(附序列自动机小讲)

因为litble是个辣鸡所以本文于2018.7.23被重写
因为litble是个辣鸡,本文只能帮你理清后缀自动机一些“是什么”的问题,方便你迅速对这种数据结构有一个感知,关于“为什么”,请参考参考资料里的大佬们的文章
只想看序列自动机的朋友请跳到本文最后

后缀自动机的基本概念

后缀自动机是一个可以处理一个字符串的有向无环图,它存在一个起始点,由很多个节点和很多条边组成,这些节点叫做状态,这些边叫做转移
后缀自动机每条边上都写有一个字母,那么我从起始点出发,任意走几步,一定会走出原串的一个子串。通过不同的走法,我可以获得原串的所有子串。而我到一个节点终止时获得的子串,是该节点可代表的子串。
一个节点可代表的子串,是原串某个前缀的若干长度连续的后缀。
对于字符串orzabsab,zab,rzab就是前缀orzab长度连续的后缀。当然,对原串建立后缀自动机,它们不一定被同一个节点代表。
后缀自动机还存在一种名为pre指针的东西,这个东西和AC自动机中的fail指针含义相同。
后缀自动机还存在一种名为right集合的东西,假设该节点代表的所有子串,都可以作为原串以x处结尾的前缀的一个后缀,那么x存在于该节点的right集合中。

后缀自动机的基本性质

1.假设一个节点u代表的子串最小长度为min(u),最大长度为max(u),那么max(pre(u))+1=min(u)
2.任何节点的right集合一定是其pre的right集合的真子集。将所有pre指向u的节点的right集合并起来,就是u的right集合

字符串abbb的后缀自动机大约如图所示:
灵魂画手litble

建立后缀自动机

//step:即从起始节点走到当前节点的最长路
void ins(int x) {//已经将原串一部分插入了后缀自动机,现在再插入一个字符x
    int np=++SZ,p=last; step[np]=step[p]+1,last=np;
    //last:上一个字符建立的实节点位置,实节点的最长路一定是在原串中以x所在位置结尾的前缀长度
    while(!ch[p][x]&&p) ch[p][x]=np,p=pre[p];//顺着pre指针添加儿子
    if(!p) pre[np]=1;//如果调到根了,那么np的pre就是根
    else {
        int q=ch[p][x];
        if(step[q]==step[p]+1) pre[np]=q;//如果q可以作为np的pre
        else {
            int nq=++SZ; step[nq]=step[p]+1;//建立虚节点nq
            for(RI i=0;i<26;++i) ch[nq][i]=ch[q][i];//用nq代替q
            pre[nq]=pre[q],pre[q]=pre[np]=nq;
            while(p&&ch[p][x]==q) ch[p][x]=nq,p=pre[p];
        }
    }
}

参考资料

后缀自动机学习笔记 -Menci
WC2012后缀自动机讲解课件 -陈立杰
后缀自动机详解 -DZYO
对后缀自动机的一点理解 -PIPIBoss

题外话:序列自动机

后缀自动机的一条路径是原串的一个子串,那么序列自动机上的一条路径就是原串的一个子序列
序列自动机很好写,就是每次查看最后出现过的一些表示字母x的节点,如果它们没有当前插入的字符y的儿子,那么就将它们的y儿子赋为当前节点,显然这样可以表示出原串的所有子串。

void ins(int x) {
    ++SZ,pre[SZ]=last[x];
    for(RI i=0;i<26;++i) {
        int now=last[i];
        while(!ch[now][x]) ch[now][x]=SZ,now=pre[now];
    }
    last[x]=SZ;
}

猜你喜欢

转载自blog.csdn.net/litble/article/details/78997914