芝士:后缀自动机(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;
}

浙公网安备 33010602011771号