后缀自动机 SAM

后缀自动机的构建算法

哎哎哎你什么都没讲就开始讲构造?

这是因为后缀自动机的构造算法是一个名为增量算法的东西,说白了就是一个一个插入字符,这样的话我们就只需要考虑两件事

1.新建几个节点

2.新建的节点连到什么节点上

先来考虑新建几个节点

更加准确的说,加入的这第i个字符会产生几种新子串

第一种是S的第i个前缀,right集合只有i

第二种是S的第i个前缀的某些后缀,right集合有一堆,不只有1

所以好了我们新建两个节点npnq分别代表第一种和第二种子串

问题来了,谁会向它连边,它又会向谁连边呢?

尽管后缀自动机节点的意义很复杂,但是,如果把表示的子串写出来,会发现不过是某一个字符串的一些后缀而已,因此边的转移的意义就是在这个字符串上加了一个字符x之后变成了一个新的字符串而已,而这些后缀的长度都增加了1,而个数不变,仅此而已。

所以我们找到表示原来第i-1个前缀的节点,让它连一条标号为x的边到np

但是我们会发现一个非常辣手的问题,别的点的right集合如果包含i-1,那么也存在着转移的可能

所以必须找到每一个right集合包含i-1的节点,要怎么找呢

Parent

我们发现如果将某个节点的表示的串写出来,是某一个字符串的一些后缀

如果我们在字符串前加入一个字符,那么right集合可能会缩小,因为这个串在原串可以匹配的位置可能减少

另外尽管我们只保存了len最大值,但是len最小值也是客观存在的

考虑这个串一个小一点的后缀,比如长度是len的最小值-1,那么这个后缀代表的节点的right集合必然包含原来节点的right集合,因为可以匹配的位置必然增多,否则这个后缀也可以压缩到这个节点里

然后这个right集合的包含关系可以映射为树上的父子关系

所以在后缀自动机中我们令fa[x]表示可以包含节点xright集合,且集合元素最小的点(哦对了,显然right集合不是包含就是不交,因为如果有交的话两个节点表示的子串写出来一样,其实就是一个节点)

根据这个父子关系可以构成一个树称为parent

有了parent树,就不需要存储right集合了,因为现在我们可以找到这个点子树中所有的叶子结点(显然表示的前缀节点是且仅是叶子结点),就可以找到这个点的right集合了

有了parent树,刚才的问题好解决多了,我们枚举第i-1个前缀到树根的所有节点即可

如果这个路径上的节点没有可以转移到x的出边,意味着原来不存在这样的子串,但现在有了,叫np,所以直接连一条标号为x,到np的出边即可

如果我们就这样跑完了整个路径,令fa[p]=start(开始节点)return即可,这种情况nq不存在

另一种比较辣手的情况,路径上节点出现了可以转移到x的出边,并且指向q

此时我们会发现如果两个集合的right集合全等(也就是maxlen(当前节点)+1=maxlen(q),因为如果最长的串right集合都可以转移,那么其他小一点串当然也可以转移),我们只需要在qright集合,以及q的祖先的right集合中插入i即可,所以我们只需要让fa[np]=q,就完成了隐性插入的工作(抱歉这种情况n,q也不存在)

另一种更加辣手的情况,两个集合的right集合不全等

此时证明q已经过时了,该转移到的应该是nq,因为加了一个字符之后写出来的应该是nq代表的字符串,而right集合比q的多,同时,q转移到的点nq都可以,因为这两个串写出来一膜一样,所以复制一边q的出边到nq上,另外当前节点最长的点都可以转移到nq上,所以nqlen应该等于len[当前节点]+1

此时还没有完,当前结点的祖先如果有连向q的出边,那么都会因为同样的理由过时了,所以全部修改为nq

最后是parent树的问题,显然nqright集合是qright集合\(∪\)i(n,p的right集合)所以令qnpparent都为nq,同时q的所有祖先还是可以转移的,只是需要加入一个i,同理使用这样的隐性插入法,只需令fa[nq]=fa[q]即可

在经历了上述乱七八糟的算法之后,我们终于完成了插入一个字符

整个SAM的构建过程是 \(O(n)\)

在理解上述算法的时候注意两点

1.建什么新点,连什么新边,parent的树边怎么修改

2.一个节点可能意义比较繁杂,但是写出来的串只有一个串和它的一堆后缀

了解了这两点之后,反复阅读各路神犇的博客,就应该可以掌握SAM的构建了

【模板】后缀自动机 (SAM)

题目描述

给定一个只包含小写字母的字符串 \(S\)

请你求出 \(S\) 的所有出现次数不为 \(1\) 的子串的出现次数乘上该子串长度的最大值。

输入格式

一行一个仅包含小写字母的字符串 \(S\)

输出格式

一个整数,为所求答案。

样例 #1

样例输入 #1

abab

样例输出 #1

4

提示

对于 \(10 \%\) 的数据,\(\lvert S \rvert \le 1000\)
对于 $100% $的数据,\(1 \le \lvert S \rvert \le {10}^6\)

解法

我们令叶子节点的size=1.暴力建出parent树然后\(dfs\),求出每个节点的right集合size,然后求\(len×sizelen×size\)的最大值就行了

#include<cstdio>
#include<algorithm>
using namespace std;const int N=3*1e6+10;typedef long long ll;
char mde[N];int nl;ll res;
struct suffixautomation
{
    int mp[N][30];int fa[N];int ed;int ct;int len[N];int siz[N];
    suffixautomation(){ed=ct=1;}
    int v[N];int x[N];int al[N];int cnt;
    inline void add(int u,int V){v[++cnt]=V;x[cnt]=al[u];al[u]=cnt;}
    inline void ins(int c) 
    {
        int p=ed;siz[ed=++ct]=1;len[ed]=nl;//先初始化size和len 
        for(;p&&mp[p][c]==0;p=fa[p]){mp[p][c]=ed;}//然后顺着parent树的路径向上找 
        if(p==0){fa[ed]=1;return;}int q=mp[p][c];//case1 
        if(len[p]+1==len[q]){fa[ed]=q;return;}//case2
        len[++ct]=len[p]+1;//case 3
        for(int i=1;i<=26;i++){mp[ct][i]=mp[q][i];}
        fa[ct]=fa[q];fa[q]=ct;fa[ed]=ct;
        for(int i=p;mp[i][c]==q;i=fa[i]){mp[i][c]=ct;}
    }
    inline void bt(){for(int i=2;i<=ct;i++){add(fa[i],i);}}//暴力建树 
    void dfs(int u)//dfs 
    {
        for(int i=al[u];i;i=x[i]){dfs(v[i]);siz[u]+=siz[v[i]];}
        if(siz[u]!=1){res=max(res,(ll)siz[u]*len[u]);}
    }
}sam;
int main()
{
    scanf("%s",mde+1);//没啥好说的,建sam然后dfs 
    for(nl=1;mde[nl]!='\0';nl++){sam.ins(mde[nl]-'a'+1);}
    sam.bt();sam.dfs(1);printf("%lld",res);return 0;//拜拜程序~ 
}
posted @ 2022-06-12 21:24  PassName  阅读(52)  评论(0编辑  收藏  举报