后缀自动机(知识整理+板子总结)

思路来源

https://www.luogu.com.cn/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie 首推这一篇

https://www.cnblogs.com/zjp-shadow/p/9218214.html 也看了这一篇,也不错

http://hihocoder.com/problemset/problem/1441 hihocoder板子题

https://www.luogu.com.cn/problem/solution/P3804 洛谷板子题

前置知识

自动机的知识(编译原理那一套,比如有限状态自动机云云,知道能在其上沿边转移感觉就可)

AC自动机、Trie树、后缀数组

 

死记硬背环节

证明和心得(后附)感觉用处不大,直接计入死记硬背和抄板子做题环节

后缀自动机(SAM),是一个最小的确定有限状态自动机(DFA),接受且只接受S的后缀

parent树和自动机节点共用,时间复杂度O(n),空间复杂度O(n)

 

parent(也称fa,后缀链接link):当一个节点的串,在变为其后缀,且endpos发生了扩大时,最长的那个后缀对应的节点

endpos(也称right):每个节点对应的串的endpos集合是一致的,每个子串唯一出现在某个节点里

len:一个节点内代表的串的最大长度

minlen:一个节点内代表的串的最小长度,由于a回跳到父亲fa(a)的时候len(fa)+1=minlen(a),故一般不存

ch[0-26](后缀转移transfer):自动机上对应字母的转移

 

配合两张图食用,便于记忆这些概念

图1:abcd的后缀自动机,

红色括号是endpos集合,字母是自动机转移

图二:aaba的后缀自动机,

红色数字是节点对应的最长子串的长度len,蓝边是其parent树上的父亲fa

板子

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct NODE
{
    int ch[26];
    int len,fa;
    NODE(){memset(ch,0,sizeof(ch));len=0;}
}dian[N<<1];
int las=1,tot=1;
void add(int c)
{
    int p=las;int np=las=++tot;
    dian[np].len=dian[p].len+1;
    for(;p&&!dian[p].ch[c];p=dian[p].fa)dian[p].ch[c]=np;
    if(!p)dian[np].fa=1;//以上为case 1
    else
    {
        int q=dian[p].ch[c];
        if(dian[q].len==dian[p].len+1)dian[np].fa=q;//以上为case 2
        else
        {
            int nq=++tot;dian[nq]=dian[q];
            dian[nq].len=dian[p].len+1;
            dian[q].fa=dian[np].fa=nq; 
            for(;p&&dian[p].ch[c]==q;p=dian[p].fa)dian[p].ch[c]=nq;//以上为case 3
        }
    }
}
char s[N];int len;
int main()
{
    scanf("%s",s);len=strlen(s);
    for(int i=0;i<len;i++)add(s[i]-'a');
}

性质

①endpos:一个子串在原串中所有出现的地方,其末位置下标的集合

如串abcab,endpos(ab)={2,5}

 

②endpos相同,一个必为另一个后缀,

考虑abcdbcd,当abcd变成后缀bcd的时候,endpos增加,bcd变成cd的时候,endpos不变

 

③根据②,任意两个子串的endpos集合,要么一个是另一个子集,要么二者相交为空,

只要有一个相交的位置,说明是后缀,后缀就是含于的情况

 

④根据②,在子串缩短为其后缀的过程中,要么endpos不变,要么增加,把endpos相同的视为一个等价类节点

在SAM中,一个子串只属于一个节点,这个节点内的子串的endpos都相同

 

⑤endpos等价类(节点)个数为O(n),

这个没看懂曾经看懂过,可以参考原博客

 

⑥parent树、自动机

后缀自动机的parent树和自动机的节点是共用的,

parent树往祖先跳的时候,endpos会变大,实际上是当前串变为其后缀串,在节点中用fa定义

自动机就是读进这个字符转移到哪个点,定义了一种转移关系,在节点中用ch[0-26]定义,类似trie

 

⑦由②,不难发现,endpos不变的一段子串是连续的,

分别记minlen和len为这个点对应的子串的最小长度和最大长度

记点a覆盖的串的长度为[minlen(a),len(a)],则从minlen(a)再缩短一个长度的时候,endpos扩充,跳到父亲

所以有len(fa(a))+1=minlen(a),fa(a)是a在parent树上的父亲

 

⑧后缀自动机的边数为O(n)

这个没看懂曾经看懂过,可以参考原博客

 

⑨SAM的构造过程

设当前字母为c,从上一个点las转移过来,当前新开一个点np,endpos多出一个{n}来

先考虑转移,为了最大程度的利用节点,

只对las的祖先节点中,计当前祖先节点为p,

若不存在字母c的转移的点,对其进行转移到np的操作,

并令p回跳到p的父亲,最终p停留在了某个节点

 

以下分三种情况,实际上是(1)(2)两种情况,其中(2)分为两种子情况

 

(1)c是没出现过的字符,这样回跳的时候一定会回到根,这里计根为1号节点

根代表了空串,其endpos集合为{1,2,...,n},

因此,c所在节点np构成了一个新的endpos集合{n},直接令fa(np)=1

 

(2)停留在了中途某个点,此时点p存在字母c的转移,所以停下了,

这其中分成两种子情况,计q为p通过字母c能转移到的点,len[q]是q中最长串的长度

 

①若len[q]=len[p]+1,由于p是las的祖先,p的串一定是las串的后缀,

np比las多了一个字母c,q比p多了一个字母c,且q最长的串是p最长的串的长度+1,

这表明,在p的串后面补一个字母c,其仍然是 在las串后面补一个字母c 的后缀,

则q的串也是np串的后缀,q的endpos一定是np的endpos的超集,令fa(np)=q即可

 

②len[q]>len[p]+1,由于len[q]>=len[p]+1(是由p的串补了一个字母c而来),既然不等于,就一定大于

说明q中的串分成了两部分,长度=len[p]+1的那些串x,由①,是np串的后缀,其endpos多出了{n}

长度>len[p]+1的那些串y,不是np串的后缀,其endpos没有变化,

而q是np在往上回跳的过程中,第一个包含了np的所有endpos的集合的点,

既然不能表示,就考虑将q拆成两部分,

一部分是x串集合构成的点,是新开的点,记为nq,有len[nq]=len[p]+1

另一部分是y串集合构成的点,保持原来的q不变,

 

起先,令nq的所有自动机转移关系(所有ch节点)等于原先的q,这样做可行的原因是:

nq是q的后缀,后续在加入新的字符z时,设其跳到状态nr,

由于nq和q只有碰到字母c时有区别,而z不等于c(设z=c,则nr是第一个包含了np的所有endpos的点,与nq是第一个包含了np的所有endpos集合的点矛盾),

故没有受到新字母c的影响,所以二者唯一的区别endpos{n},没有给后续的nr的状态带来影响

 

然后考虑fa的关系,nq是旧q的一部分,fa(nq)=fa(q),新q比nq长,可以令fa(q)=nq

构建这个nq节点的意义在于表示np,所以令fa(np)=nq

 

旧q的儿子(转移关系ch)原封不动保留到了新q和nq中,考虑fa的改变

原先有一些节点指向q,但现在nq成为了q和np的fa,这些点应该指向nq,

所以应从p往其祖先回跳,若其碰到字母c的转移为q,就应将其改为nq,

对于没有通过c转移到q的那些祖先节点,其一定指向了nq的祖先,故不用修改

心得

感觉自己死抠知识点的证明,不做题,没啥大的意义,

有些知识点感性理解一下就好了……

但总之思路来源的博主写的很好,

满足了我这个zz对于原理的一切幻想……

 

去年8月、11月自学过SAM的原理,但是后来没做题,就渐渐忘了,

现在想想,SA前年当时花了四五天抠原理,但现在也只记得rank、sa、height数组是干啥的了,

 

实际上,SAM的情况就分三种,代码也很短,掌握了性质就可以了……

证明什么的,除了O(n)的点数和边数的证明略复杂以外,剩下的还好……

没必要每次做题前都复习一下证明,更何况这玩意比SA做题直接很多……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章