初学后缀自动机

前言

原本以为后缀自动机后缀数组,大致是\(AC\)自动机\(KMP\)的关系。

然而... ...

后缀自动机的板子似乎比后缀数组的板子还短?(一脸懵逼)

好吧,后缀自动机的确比后缀数组简单许多。


简介

后缀自动机是个神奇的数据结构,它存储着一个字符串所有后缀

它长得有点像一个后缀版的\(AC\)自动机,但两者还是有些不同的。

例如后缀自动机的一条边上可以有多个字符,而\(AC\)自动机一条边上只有一个字符。

又如后缀自动机的\(fail\)指针(\(Father\)数组)是一边插入字符一边更新的,而\(AC\)自动机则是最后统一建的。

诸如此类的小细节还有很多,这里就不一一举例了。

后缀自动机与\(AC\)自动机一样有一个基本性质:从根出发,走到每一个终止节点的路径恰好是所有的字符串(后缀自动机的字符串特指后缀),且保证不重不漏。

用后缀自动机可以干许多事,如判断字符串是否出现不同子串个数等等。

下面,让我们来认识一下后缀自动机吧。


\(Father\)数组

后缀数组的大致思想就是将字符串的字符一个个插入后缀自动机中,并注意每插入一个字符就要对后缀自动机内的信息进行维护。

对于每个节点,我们用一个变量\(len\)存储字符串长度\(Father\)存储上一个节点的编号\(Son\)存储当前节点能到达的节点列表

而其中最核心的便是每个节点的\(Father\),它的定义如下:

  • 如果存在若干后缀是当前后缀的后缀,则我们选择其中最长的一个后缀作为当前节点的\(Father\)
  • 如果不存在,则当前节点的\(Father\)根节点

明确了\(Father\)的概念,我们就可以开始学习后缀自动机的基本思路了。


基本思路

假设我们要插入一个元素\(x\)

首先,我们新建一个节点\(now\),为当前即将插入的新节点。

它的\(len\)就是上次插入的节点\(lst\)\(len\)\(1\)

接下来,我们用一个变量\(p\)指向\(lst\),然后我们就可以把\(lst\)更新为\(now\)了。

然后我们判断\(p\)是否有编号为\(x\)的儿子,如果没有,就更新\(p\)编号为\(x\)的儿子为\(now\),并更新\(p\)为其\(Father\)

重复此操作直至\(p=0\)或者\(p\)有一个编号为\(x\)的儿子。

判断\(p\)是否为\(0\),如果\(p=0\),则我们令\(now\)\(Father\)为我们事先构造好的根节点\(1\)号节点,然后退出函数。

不然,我们用\(q\)记录\(p\)编号为\(x\)的儿子,然后判断\(q\)节点存储的\(len\)是否恰好是\(p\)节点的\(len\)\(1\),如果是,则无需构造新节点,可直接令\(now\)\(Father\)\(q\),然后退出函数。

否则,我们在\(p\)\(q\)之间新建一个节点\(k\),作为当前插入节点的\(Father\)

初始化\(k\)的信息,令\(k\)\(len\)\(p\)\(len\)\(1\),且其\(Father\)\(Son\)皆从\(q\)复制过来。

然后更新\(now\)\(q\)的父亲为\(k\)

还有比较重要的一点是,要将\(p\)及其祖先所有与\(q\)相连的边全部改成与\(k\)相连,方便以后的状态查找。


操作流程

以下便是上述内容的具体实现。

首先,我们要定义一个结构体来存储后缀自动机上的每一个节点:

struct Status
{
    int len,Father,Son[CHAR_SET_SIZE+5];//len存储字符串长度,Father存储上一个节点,Son[i]存储其能到达的节点列表
}node[(SAM_SIZE<<1)+5];//注意数组要开两倍

然后便是\(Insert\)函数:

inline void Insert(int x)//增加一个元素
{
    register int p=lst,q,k,now=lst=++tot;node[now].len=node[p].len+1,node[now].Size=1;
    while(p&&!node[p].Son[x]) node[p].Son[x]=now,p=node[p].Father;//如果p!=0,且没有一个编号为x的儿子,就不断向上跳
    if(!p) return (void)(node[now].Father=1);//如果p=0
    if(node[p].len+1==node[q=node[p].Son[x]].len) return (void)(node[now].Father=q);//如果q节点存储的len是否恰好是p节点的len加1
    node[k=++tot].len=node[p].len+1,node[k].Father=node[q].Father,node[now].Father=node[q].Father=k,memcpy(node[k].Son,node[q].Son,sizeof(node[q].Son));//新增一个节点k作为now的Father,并初始化其信息
    while(p&&!(node[p].Son[x]^q)) node[p].Son[x]=k,p=node[p].Father;//将p及其祖先所有与q相连的边全部改成与k相连
}

如何实现询问

以上便是建后缀自动机的全部内容了。

然后我们要考虑一个十分现实的问题:后缀自动机如何实现询问?

注意到后缀自动机节点与其\(Father\)的连边是一棵树,因此,我们可以将其先按照\(len\)进行一遍基数排序,然后按\(len\)从大到小,向根节点传递信息。

最后的答案,就储存于根节点中。


代码(板子题

#include<bits/stdc++.h>
#define N 1000000
#define Gmax(x,y) (x<(y)&&(x=(y)))
#define LL long long
using namespace std;
char s[N+5];
class Class_SuffixAutomation//后缀自动机
{
    private:
        #define SAM_SIZE N
        #define CHAR_SET_SIZE 30
        int tot,lst,s[(SAM_SIZE<<1)+5],cnt[(SAM_SIZE<<1)+5];
        struct Status
        {
            int len,Size,Father,Son[CHAR_SET_SIZE+5];
        }node[(SAM_SIZE<<1)+5];
        inline void RadixSort(int n) {register int i;for(i=1;i<=n;++i) ++cnt[node[i].len];for(i=1;i<=n;++i) cnt[i]+=cnt[i-1];for(i=1;i<=n;++i) s[cnt[node[i].len]--]=i;}
    public:
        Class_SuffixAutomation() {tot=lst=1;}
        inline void Insert(int x)
        {
            register int p=lst,q,k,now=lst=++tot;node[now].len=node[p].len+1,node[now].Size=1;
            while(p&&!node[p].Son[x]) node[p].Son[x]=now,p=node[p].Father;
            if(!p) return (void)(node[now].Father=1);
            if(node[p].len+1==node[q=node[p].Son[x]].len) return (void)(node[now].Father=q);
            node[k=++tot].len=node[p].len+1,node[k].Father=node[q].Father,node[now].Father=node[q].Father=k,memcpy(node[k].Son,node[q].Son,sizeof(node[q].Son));
            while(p&&!(node[p].Son[x]^q)) node[p].Son[x]=k,p=node[p].Father;
        }
        inline LL GetAns()//求答案
        {
            register int i;register LL ans=0;
            for(RadixSort(tot),i=tot;i;--i) node[node[s[i]].Father].Size+=node[s[i]].Size,node[s[i]].Size^1&&Gmax(ans,1LL*node[s[i]].len*node[s[i]].Size);//向上更新信息
            return ans;
        }
}SAM;
int main()
{
    register int i,len;
    for(scanf("%s",s),len=strlen(s),i=0;i<len;++i) SAM.Insert(s[i]&31);
    return printf("%lld",SAM.GetAns()),0;
}

猜你喜欢

转载自www.cnblogs.com/chenxiaoran666/p/SuffixAutomation.html
今日推荐