【算法与数据结构】——后缀自动机

参考博客史上最通俗的后缀自动机详解
参考视频后缀自动机
这篇博客讲的挺详细的,我看了一遍就基本明白了整个后缀自动机的原理和构建方法。
我在这里不详细记录后缀自动机的原理了。只记录下常用的代码部分。
后缀自动机的构造是在线的,即我们通过不断添加单个字符的方式构建后缀自动机,时刻调整其状态。
构建后缀自动机的代码:

struct NODE
{
    int ch[26];
    int len,fa;//len就是当前节点表示的endpos等价类中最长字串的长度,fa就是类的父类所表示的节点s
    NODE(){memset(ch,0,sizeof(ch));len=0;}
}dian[MAXN<<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[MAXN];int len;
int main()
{
    scanf("%s",s);len=strlen(s);
    for(int i=0;i<len;i++)add(s[i]-'a');
}

后缀自动机的使用

后缀数组的时间复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn)。后缀自动机的时间复杂度是O(n)O(n),可能常数会稍微大一点点,但应该还是比后缀数组快(取决于个人代码常数)。但是后缀自动机的做法往往比后缀数组无脑。

问题1:判断子串

直接在后缀自动机上跑边,跑完串还未跑到NULL则为原串子串。

后缀数组:跑出sa,然后从最小的后缀开始,一个个往后枚举,记录下当前匹配到的位置,如果匹配不上就下一个后缀,否则位置向后移一位。如果枚举完了后缀还没有完全匹配则不是原串子串。

问题2:不同子串个数

DAG上DP。对于一个节点i,f[i]表示从i出发的子串个数(不含空串)。那么,f[i]就等于 ∑ ( i , j ) ∈ E d g e ( f [ j ] + 1 ) \sum_{(i,j)\in Edge}(f[j]+1) (i,j)Edge(f[j]+1)。f[1]即是答案。
或者直接求 ∑ ( l e n ( i ) − l e n ( f a ( i ) ) ) \sum(len(i)-len(fa(i))) (len(i)len(fa(i))),因为后缀自动机上无重复字符串。
后缀数组:每一个后缀的长度减去其height之和。

问题3:在原串所有子串中(相同的不算一个)字典序第ii大的是哪个(P3975)

这题比较重要。

首先处理出每个节点的endpos大小,即每个类中的串在原串中出现的次数。考虑dp,f[i]代表i的大小。对于不包含任意一个个前缀的节点, f [ i ] = ∑ ( i , j ) ∈ p a r e n t   t r e e E d g e f [ j ] f[i]=\sum _{(i,j)\in parent \space treeEdge}f[j] f[i]=(i,j)parent treeEdgef[j],因为比longest(i)longest(i)前面多一个字符的所有字符串的endpos的并集必然等于endpos(longest(i))。而对于包含前缀的节点, f [ i ] = ( ∑ ( i , j ) ∈ p a r e n t   t r e e E d g e f [ j ] ) + 1 f[i]=(\sum _{(i,j)\in parent \space treeEdge}f[j])+1 f[i]=((i,j)parent treeEdgef[j])+1f,因为第1位的信息在加字符时丢失了。(这里是第二节遗留的问题,可以参考第二节的parent tree示意图来理解。因为parent tree上一个节点的出边(方向从fa到ch)相当于对此节点endpos的拆分,拆分方式为在此节点的longest前面添加字符。但由于此节点包含前缀,在添加字符时必将丢失此前缀位置的endpos(因为加上字符的子串已经大于前缀长度),所以丢失了1位信息,需要加回来( u p d a t e   2019.8.15 update\space 2019.8.15 update 2019.8.15)(具体到程序来说,只需在case1里加一句f[np]=1即可)
然后DP出g[i],表示从i出发的子串个数(不含空集且计算重复),则 g [ i ] = ∑ ( i , j ) ∈ S A M   E d g e ( g [ j ] + f [ j ] ) g[i]=\sum_{(i,j)\in SAM\space Edge}(g[j]+f[j]) g[i]=(i,j)SAM Edge(g[j]+f[j])
最后,在后缀自动机上dfs,按字典序遍历出边,排名还够的减去这边的所有子串,不够的跑到这条出边连向的节点,重复以上步骤。当排名变为非正数时输出跑过的路径形成的字符串。具体做法还请参考本题题解。

问题4:判断两个串的最长公共子串

把两个串拼起来,跑后缀自动机。然后用类似于上面处理出现次数的方法,跑出一个子串在拼起来的串前半部分出现的次数和后半部分出现的次数。然后遍历节点,找lenlen最大的前后出现次数都不为00的节点。以上思路还可以处理多个字符串的最长公共子串。

后缀数组:同样是拼起来,然后处理sa和height,对于每个后缀,找到其之后第一个属于另一半部分的后缀(可以O(n)做到,具体做法请读者思考),求它们的lcp,最后取最大值。

你可能感兴趣的:(算法与数据结构,算法,数据结构)