后缀自动机 SAM
后缀自动机的构建算法
哎哎哎你什么都没讲就开始讲构造?
这是因为后缀自动机的构造算法是一个名为增量算法的东西,说白了就是一个一个插入字符,这样的话我们就只需要考虑两件事
1.新建几个节点
2.新建的节点连到什么节点上
先来考虑新建几个节点
更加准确的说,加入的这第i
个字符会产生几种新子串
第一种是S
的第i
个前缀,right
集合只有i
第二种是S
的第i
个前缀的某些后缀,right
集合有一堆,不只有1
所以好了我们新建两个节点np
,nq
分别代表第一种和第二种子串
问题来了,谁会向它连边,它又会向谁连边呢?
尽管后缀自动机节点的意义很复杂,但是,如果把表示的子串写出来,会发现不过是某一个字符串的一些后缀而已,因此边的转移的意义就是在这个字符串上加了一个字符x
之后变成了一个新的字符串而已,而这些后缀的长度都增加了1
,而个数不变,仅此而已。
所以我们找到表示原来第i-1
个前缀的节点,让它连一条标号为x的边到np
但是我们会发现一个非常辣手的问题,别的点的right
集合如果包含i-1
,那么也存在着转移的可能
所以必须找到每一个right
集合包含i-1
的节点,要怎么找呢
Parent
树
我们发现如果将某个节点的表示的串写出来,是某一个字符串的一些后缀
如果我们在字符串前加入一个字符,那么right
集合可能会缩小,因为这个串在原串可以匹配的位置可能减少
另外尽管我们只保存了len
最大值,但是len
最小值也是客观存在的
考虑这个串一个小一点的后缀,比如长度是len
的最小值-1
,那么这个后缀代表的节点的right
集合必然包含原来节点的right
集合,因为可以匹配的位置必然增多,否则这个后缀也可以压缩到这个节点里
然后这个right
集合的包含关系可以映射为树上的父子关系
所以在后缀自动机中我们令fa[x]
表示可以包含节点x
的right
集合,且集合元素最小的点(哦对了,显然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
集合都可以转移,那么其他小一点串当然也可以转移),我们只需要在q
的right
集合,以及q
的祖先的right
集合中插入i即可,所以我们只需要让fa[np]=q
,就完成了隐性插入的工作(抱歉这种情况n,q
也不存在)
另一种更加辣手的情况,两个集合的right
集合不全等
此时证明q已经过时了,该转移到的应该是nq
,因为加了一个字符之后写出来的应该是nq代表的字符串,而right集合比q的多,同时,q
转移到的点nq
都可以,因为这两个串写出来一膜一样,所以复制一边q
的出边到nq
上,另外当前节点最长的点都可以转移到nq
上,所以nq
的len
应该等于len[当前节点]+1
此时还没有完,当前结点的祖先如果有连向q
的出边,那么都会因为同样的理由过时了,所以全部修改为nq
,
最后是parent
树的问题,显然nq
的right
集合是q
的right
集合\(∪\)i(n,p的right集合)
所以令q
和np
的parent
都为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;//拜拜程序~
}