后缀自动机学习笔记

后缀自动机感觉比回文自动机和AC自动机难理解很多,我花了一个下午加一个晚上感觉还没有完全理解。
蒟蒻还是太菜了,但是我还是要写这篇博客,也希望能加深我的理解。


1.什么是后缀自动机

hihocoder的出题人很有良心,在一道题目里详解了什么是后缀自动机。想看的点这里。我也搬过来讲讲。

首先我们先把后缀自动机的图放出来,对于字符串S=”aabbabd”,它的后缀自动机是:(搬自hihocoder)
这里写图片描述
看起来十分的复杂,我们先不管绿线,只看蓝色的线,我们发现从S出发,沿着任意一条线走,一直走到T(图中的9),走出来的路径都是该字符串的后缀,不信可以试试看,举个例子:S5679对应的字符串是babd,就是aabbabd的后缀。
那么我们从S出发,沿着任意蓝线走,走到任意位置,走出的字符串都是该文本串的子串,比如S1846是abba,就是一个子串。
所以结合这张图,我们就能大概弄懂后缀自动机是个什么东西、它能干啥。

2.后缀自动机相关的一些东西

看上面的图,我们知道后缀自动机要构筑出所有的后缀,那么在构筑之前,我们要先看一些东西,以便后面理解。
首先一个概念是子串的位置结束集合。也就是一个子串,它在哪些位置结束过,比如对于aabbabd,那么子串ab的结束位置集合为{3,6}因为它在这样个位置都结束过,子串aabb的结束位置集合就只有{3}了。我们以endpos(s)表示s子串的结束位置集合。
我们把所有的子串的endpos都求出来,如果又两个子串的endpos相等,那么我们就把这两个子串的endpos归为一类。
那么接下去就有一个结论了,如果串s是串t的后缀,那么endpos(s) ⊇ endpos(t),这个十分显然,因为只要t出现过的地方,s都出现过。而s出现过的地方,t并不一定出现过。并且还有一个结论,如果s不是t的后缀,那么 endpos(s) ∩ endpos(t) = ∅,这也非常显然。并且这两个命题的逆命题也一样成立。

那么我们就能发现一些性质了,endpos相同的子串集合一定是一些长度递减,并且长度短的为长度长的的后缀的一些子串。随便举个例子,就以endpos=6的集合为例,又哪些字符串呢?aabbab,abbab,bbab,bab。你看后面的串是前面的串的后缀,而且长度递减。
但是ab就不行了,因为ab在位置三出现过。所以我们发现对于每个endpos相同的集合,它里面的字符串的长度都是一段连续的数,对于endpos为i的集合,我们把子串长度最大的记为longest[i],最小的记为shortest[i],那么shortest[i]肯定等于某个longest[j]+1,比如我们前面endpos为6的集合的shortest就是endpos为{3,6}的集合的longest加一。

如果shortest[i]=longest[j]+1,那么我们就记fa[i]=j,那大家可以发现,我们上图中绿色的连线其实就是fa这个数组的连线,而图中每个点代表的endpos集合就是从S走到这个点的字符串的endpos。

如果我们遮掉蓝色的边不看,只看绿色的边,我们会发现其实这是个树形结构,对于这棵树,我们称之为parent树,这棵树在构建后缀自动机的时候又很大的用处。

3.后缀自动机的构建

下面最重要的部分后缀自动机的构建要来了。对于构建,我们要分三种情况:

假设我们要加入的字符为c,它是第p个点。而在之前加入的字符中没有出现过c,那么怎么办呢,我们先找到上一个出现的字符,然后一直往回跳它们的fa,知道跳到S点,然后把这些点的c儿子都赋值为p,最后fa[p]=1即可。

    while(f&&!son[f][c]) son[f][c]=p,f=fa[f];
    if(!f){fa[p]=1;return;} //为什么fa[p]等于1?因为在以p结尾的endpos中,p是最短的,shortest为1,
    //而1(也就是空节点)的longest为0,所以fa[p]=1

这种情况还是比较简单的。
而如果出现过c字符,那么我们的f会在某一个地方停下来。

这种情况也比较特殊,就是前面加入的字符串的所有字符都为c,比如之前的字符串为aaa,现在又加入字符a。
那么这个时候会出现什么情况呢?设x为son[f][c],那么 longest(f)+1=shortest(x)=longest(x) l o n g e s t ( f ) + 1 = s h o r t e s t ( x ) = l o n g e s t ( x ) ,因为只有从s到我们要加入字符的地方所有的字符都一样时,整个自动机才会只有一条路径。(比如如果串是aaa,那么自动机就是一条aaa的链。但如果串是aab,那么串除了是aab的链以外,还会有一条从S连到b的路径,那么这时候上式就不成立了)
那么这个时候直接让fa[p]=x即可。

    int x=son[f][c];
    if(len[x]==len[f]+1){
        fa[p]=x;return;
    }

那么剩下的情况,也就是 longest(f)+1=shortest(x)=longest(x) l o n g e s t ( f ) + 1 = s h o r t e s t ( x ) = l o n g e s t ( x ) 不成立时的情况就是第三种情况了。
那么这时的操作比较复杂:

把x节点复制一遍给y,所有前面连续的一段本应该连向x的都连向y(也就是说在A前面可能还有连向x的边),所有从x连出去的边都连向y,把x和p的parent父亲连向y,把y的len设置为len[f]+1。

这么说可能大家有点懵逼,所以结合代码和举例子,可以再理解一下:

    int y=++node,x=son[f][c];
    fa[y]=fa[x];fa[x]=fa[p]=y;len[y]=len[f]+1;
    memcpy(son[y],son[x],sizeof(son[y]));
    while(f&&son[f][c]==x) son[f][c]=y,f=fa[f];

我们以aaabb为例:
首先添加a,属于情况1,直接连即可。后面两次添加a,都属于情况2,比较简单。然后第一次添加b,又是情况1。然后当我们添加第二个b时,这个b的父亲是第一个b。那么我们就按照上述方法进行复制和置换。那么S相连的b就成了复制的b,但第三个a相连的b依旧是第一个b。而第一个b的父亲变成了复制的b。然后它们(第一个b和我们复制的b)的儿子是第二个b,而第二个b的父亲是第一个b。

大家发现这样改动的根本意义是改变了第一个b的父亲。因为在加入一个字符后,第一个b的endpos集合会变,所以需要一个新的点来帮助维护正确的parent树。

讨论完这三种情况后,我们构建就完成了。


模板

洛谷的板子题

代码如下:

#include<bits/stdc++.h>
#define MAXN 2000005
#define ll long long
using namespace std;
int read(){
    char c;int x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
    while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;
}
string s;
int lst=1,node=1,son[MAXN][26],fa[MAXN],len[MAXN],siz[MAXN],A[MAXN],t[MAXN];
ll ans;
void SAM(int c){
    int f=lst,p=++node;lst=p;
    len[p]=len[f]+1;siz[p]=1;
    while(f&&!son[f][c]) son[f][c]=p,f=fa[f];
    if(!f){fa[p]=1;return;}  //situation 1
    int x=son[f][c],y=++node;
    if(len[x]==len[f]+1){  //situation 2
        fa[p]=x;node--;return;
    }
    fa[y]=fa[x];fa[x]=fa[p]=y;len[y]=len[f]+1;  //situation 3
    memcpy(son[y],son[x],sizeof(son[y]));
    while(f&&son[f][c]==x) son[f][c]=y,f=fa[f];
}
int main()
{
    cin>>s;
    for(int i=0;i<s.size();i++) SAM(s[i]-'a');
    for(int i=1;i<=node;i++) t[len[i]]++;
    for(int i=1;i<=node;i++) t[i]+=t[i-1];
    for(int i=1;i<=node;i++) A[t[len[i]]--]=i;
    for(int i=node;i;i--){  //由于题目要求,将所有的len进行基数排序,然后从parent树上从后到前(类似于dfs)的思想统计答案
        int now=A[i];siz[fa[now]]+=siz[now];
        if(siz[now]>1) ans=max(ans,1ll*siz[now]*len[now]);
    }
    printf("%lld\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值