芝士:后缀自动机(SAM)

背景

考虑能否有一种数据结构能接受一个固定字符串所有的子串

虽然其的名称为后缀自动机(SAM)

原理

考虑**每一个子串 **有一个集合\(endpos\),表示为这个子串在哪些位置上出现过,将这些位置的最后一个字符所匹配的位置拿出来

比如\(aababa\),其中\(ba\)\(endpos=\{4,6\}\)

我们称每一个\(endpos\)为一个类

引理1

如果有一个子串\(t\)和一个子串\(s\),如果\(endpos_t\in endpos_s\),那么一定有\(s\)\(t\)的一个后缀,这应该很容易理解吧

引理2

规定\(len_s<len_t\)

将引理1反过来,如果\(s\)\(t\)的一个后缀,那么一定有\(endpos_t\in endpos_s\),应该这也是很容易理解的

反之,如果\(s\)不是\(t\)的一个后缀,那么$endpos_t\bigcap endpos_s=\varnothing $

考虑反证法,如果\(s\)不是\(t\)的一个后缀,且\(endpos_t\bigcap endpos_s\neq \varnothing\)

\(endpos_t\bigcap endpos_s=A\)

那么如果在\(A_i\)位置两者的\(endpos\)是一样的,那么\([A_i-len_t+1,A_i]\)即为\(t\),\([A_i-len_s+1,A_i]\)即为\(s\),那么\(s\)必为\(t\)的一个后缀,故假设不成立

引理3

对于相同的\(endpos\),考虑设其中长度最小的一个为\(s\),最长的一个为\(t\)

这里单独将\(t\)拿出来,只有这里的下标是以t为基准

那么有\(\forall i\in [1,len_t-len_s+1]\),有\([i,len_t]\)\(endpos\)\(t\)\(endpos\)是一样的

比如对于一个任意的类,其中最长的为\(aababbb\),最短的为\(bbb\)

那么\(aababbb,ababbb,babbb,abbb,bbb\),这5个子串的\(endpos\)是相同的

引理4

\(endpos\)不同的类最多只有\(n\)个(这里指的是\(n\)级别)

对于一个等价类,考虑往前面加只加一个字符,必然会导致\(endpos\)裂开

但是可以保证的是,所有裂开之后的\(endpos\)是没有交集的,同时,这些$endpos \(中的值一定是来源于原来的\)endpos$,

基于此,如果有两个\(endpos_s\)\(endpos_t\),满足\(endpos_s\bigcap endpos_t=\varnothing\),那么其裂出来的\(endpos\)之间一定是两两之间交集为空

也就是指对于子串的变化,实际上就是将\(endpos\)进行分裂

最初的\(endpos\)\(\{1,2\ldots n\}\),空串

利用线段树的思想,很容易得到其所有的节点数不超过\(2n\)

也就是指不同\(endpos\)不会超过\(2n\)

引理5

考虑一个\(endpos\)的最长的字符串长度为\(max\),其最短的字符串长度为\(min\)

那么有\(min_u=max_{fa_u}+1\),这里的边是指的是用引理4中提到的裂开和没裂开的\(endpos\)之间的连边

这应该也是显然的吧

现在考虑怎么去完成一个自动机应该有的功能,即能接受所有的子串

考虑现在已经将\(endpos\)将整个树建了起来

比如原串为\(abaaab\)

也许长成这个样子

其中点表示\(endpos\)的集合

考虑添加一些边进去,使得到节点u的路径都覆盖所有的\(endpos\)所代表的字符串,可以证明,其增加的边数也是\(n\)级别的,

证明:@!(#@!*(……%!(@#

实际上是因为直接笔者听证明的时候。。。。。

构造

好,现在已经知道了SAM大概的原理了,现在考虑怎么在一个优秀的时间复杂度内构造出SAM,

不会吧,不会真的有人想用上面的原理直接构造吧

根据巨佬们的一次次的探索,SAM大概长成这个样子

最下面的一行节点表示所有的前缀

现在考虑,

如果巨佬给了你一个按\([1,n-1]\)已经建好的SAM,现在你怎么添加一个字符串进去,使其依然是一个SAM,

考虑到上面所说的原理,这里只讨论最基本的自动机

每一个点需要维护3个信息,\(tre[i].len,tre[i].fa,tre[i].ch[]\)

这里的fa数组是用endpos建出来的树的边

这里的ch数组才是SAM上的边

其中\(len\)表示最长的符合\(endpos\)的子串,\(fa\)就直接表示父亲节点,\(ch[]\)表示当前节点后面添加了字符\(c\),下一个节点会达到哪里

我们考虑利用最下面一行的进行构造

设上一次的前缀为\(las\)

\(las\)往上的节点的\(endpos\)一定包含\(n-1\),考虑在最后新加一个节点,这些节点如果没有字符为\(c\)的都必须连一条边过来

情况1

如果这条链上所有的节点都没有字符\(c\)的儿子,那么说明新加节点一定会构成一个新的类,故直接连上去就行了,只需要改\(fa\)就行了

情况2

设当前的节点为\(p\),其连出去的边为\(q\)

如果\(tre[p].len+1==tre[q].len\)

这个时候直接连上去就行了,同样的,只改\(fa\)

情况3

设当前的节点为\(p\),其连出去的边为\(q\)

如果\(tre[p].len+1\neq tre[q].len\)

意味着,我们需要将\(q\)这个节点裂开

同时将一些节点的\(ch\)改掉

代码

例题传送门

#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
namespace SAM
{
    struct node
    {
        int fa;
        int len;
        int ch[30];
        node()
        {
            memset(ch,0,sizeof(ch));
        }
    }tre[2000005];
    int las=1,tot=1;
    int dp[200005],d[200005];
    int getf(char c)
    {
        return c-'a'+1;
    }
    void add(int c)
    {  
        int p=las;
        int np=las=++tot;
        dp[tot]=1;
        //cout<<"bas:"<<tot<<'\n';
        tre[np].len=tre[p].len+1;
        for(;p&&!tre[p].ch[c];p=tre[p].fa)
            tre[p].ch[c]=np;
        if(!p)
        {
            tre[np].fa=1;
            return;
        }
        else
        {
            int q=tre[p].ch[c];
            if(tre[q].len==tre[p].len+1)
            {
                tre[np].fa=q;
                return;
            }
            else
            {
                int nq=++tot;
                tre[nq]=tre[q];
                tre[nq].len=tre[p].len+1;
                tre[q].fa=tre[np].fa=nq;
                for(;p&&tre[p].ch[c]==q;p=tre[p].fa)
                    tre[p].ch[c]=nq;
            }
        }
    }
    long long getdp()
    {
        queue<int> q;
        for(int i=1;i<=tot;i++)
            d[tre[i].fa]++;
        for(int i=1;i<=tot;i++)
            if(d[i]==0)
                q.push(i);
        while(!q.empty())
        {
            int t=q.front();
            q.pop();
            int v=tre[t].fa;
            dp[v]+=dp[t];
            d[v]--;
            if(d[v]==0)
                q.push(v); 
        }
        long long ans=0;
        for(int i=1;i<=tot;i++)
            if(dp[i]!=1)
                ans=max(ans,1ll*dp[i]*tre[i].len);
        return ans;
    }
}
using namespace SAM;
char s[1000005];
int lens;
int main()
{
    cin>>(s+1);lens=strlen(s+1);
    for(int i=1;i<=lens;i++)
        add(getf(s[i]));
    cout<<getdp();
    return 0;
}
posted @ 2020-12-26 15:45  loney_s  阅读(303)  评论(0)    收藏  举报