后缀自动机学习笔记
参考了hihoCoder上的内容..题目也都是hihoCoder的。
什么是SAM
对于一个字符串S,它对应的后缀自动机(Suffix Automaton,简称SAM)是一个最小的确定有限状态自动机(DFA),接受且只接受S的后缀。
比如对于字符串S="aabbabd",它的后缀自动机是:
其中红色状态是终结状态。你可以发现对于S的后缀,我们都可以从S出发沿着字符标示的路径(蓝色实线)转移,最终到达终结状态。例如"bd"对应的路径是S59,"abd"对应的路径是S189,"abbabd"对应的路径是S184679。而对于不是S后缀的字符串,你会发现从S出发,最后会到达非终结状态或者“无路可走”。特别的,对于S的子串,最终会到达一个合法状态。例如"abba"路径是S1846,"bbab"路径是S5467。而对于其他不是S子串的字符串,最终会“无路可走”。 例如"aba"对应S18X,"aaba"对应S123X。(X表示没有转移匹配该字符)
endpos
定义endpos(s)
表示s在S中每次出现的结束位置集合。
可以发现,设|s_1|≤|s_2|
,则endpos(s_2) ⊆ endpos(s_1)
当且仅当s_1
是s_2
的后缀,否则显然endpos(s_1) ∩ endpos(s_2)=∅
。
所以每个endpos(s)集合对应的s一定是一个串的一些连续后缀。
我们把endpos相同的s
用同一个状态st表示,设maxlen(st),minlen(st)
表示s
长度的最大值和最小值,longest(st)
表示最长的s
,substr(st)
表示s
构成的集合。
Suffix Links
Suffix Links就是上图中的绿色虚线。
substr(st)
表示longest(st)
的一些连续后缀,会在长度minlen(st)
的地方断掉。而长度=minlen(st)-1
的后缀会被分到另一个状态st'
,令link(st)=st'
。
一个状态st沿着Suffix
Links一直走,substr(st)
并集刚好就是longest(st)
的所有后缀。
Transition Function
对于endpos(s)
相同的s
,endpos(s+c)
一定也相同(其中c
是同一个字符)
定义trans(st,c)=x|longest(st)+c∈x
怎么构造SAM
SAM可以通过\(O(|S|)\)的增量法构造。
#### 精简定义的状态
minlen(st)
可以通过maxlen(link(st))+1
得到。
若知道endpos(st)
中任意一个位置,则longest(st),substr(st)
都可以通过maxlen(st)
算出。
所以这三个都不用记。
具体构造方法
namespace SAM{
int to[2000005][26],fail[2000005],len[2000005],t=1,now=1;
void extend(int w){
int nx=++t;
len[nx]=len[now]+1;
int i=now;
for(;i&&!to[i][w];i=fail[i])to[i][w]=nx;//1
if(!i){//2
fail[nx]=1;
}
else if(len[to[i][w]]==len[i]+1){//3
fail[nx]=to[i][w];
}
else{//4
int a=++t,b=to[i][w];
len[a]=len[i]+1;
for(int j=0;j<26;++j)to[a][j]=to[b][j];
fail[a]=fail[b];
fail[b]=fail[nx]=a;
for(;i&&to[i][w]==b;i=fail[i])to[i][w]=a;
}
now=nx;
}
ll calc(){
ll s=0;
for(int i=1;i<=t;++i)s+=len[i]-len[fail[i]];
return s;
}
}
代码解释?:
fail[x]=trans(x), to[x][y]=trans(x,y),len[x]=maxlen(x)
k,
假设我们得到了S
的SAM,trans(start,S)=st'
,要在S
之后加个c
。
首先新建状态t。代表
endpos(st)={|S|+1}`.
- 由于
i
表示的所有串s
都满足s+c
没出现过,所以endpos(s+c)={|S|+1}
。 - 由于
S
的所有后缀s
都满足s+c
没出现过,所以显然minlen(st)=1
longest(trans(i,c))=longest(i)+c
,由于longest(i)
是longest(st')
的后缀,longest(trans(i,c))
也是longest(st)
的后缀。所以link(st)
可以直接=trans(i,c)
longest(i)+c
是old=longest(trans(i,c))
和longest(st)
的后缀,endpos(longest(i)+c)
显然和endpos(trans(i,c)),endpos(st)
都不一样,所以要另开一个节点new
。由于原来的longest(link(old))
是longest(new)
的后缀,所以link(new)
就是原来的link(old)
。link(old),link(st)
显然都是new
。由于new
是从old
拆分出来的,trans(new,x)
的初值就是trans(old,w)
。显然对于i
沿着link
走所有满足trans(i,c)=old
的都可以改成trans(i,c)=new
。
应用
求一个字符串不同子串个数
就是sum(maxlen(st)-minlen(st)+1)=sum(maxlen(st)-maxlen(link(st)))
,当然也可以直接在DAG
上DP
#### 求S
在T
中出现次数
就是求T
的SAM
中endpos(S)
的大小。
显然Suffix Links
构成了一个以start
为根的树。
对于T
的每一个前缀T[1:i]
,可以表示这个前缀的状态到start
上路径的所有状态endpos
都要加上i
。
当然也可以通过树上差分解决:每次新建st
的时候cnt(st)=0
,复制出new
的时候cnt(new)=0
,最后树上前缀和一下。
求S
中长度为k
的串出现次数最大值f[k]
。
对于每个st
,显然chkmax(f[i],cnt(st))for all i≤maxlen(st)
所以可以chkmax(f[maxlen(st)],cnt(st))
然后后缀max
一下
namespace SAM{
int to[2000005][26],len[2000005],fail[2000005],t=1,now=1,cnt[2000005],l,is[2000005];
void extend(int w){
int nx=++t;
len[nx]=++l;
is[nx]=1;
int i=now;
for(;i && !to[i][w];i=fail[i])to[i][w]=nx;
if(!i)fail[nx]=1;
else if(len[i]+1==len[to[i][w]]){
fail[nx]=to[i][w];
}
else{
int nw=++t,o=to[i][w];
len[nw]=len[i]+1;
fail[nw]=fail[o];
for(int j=0;j<26;++j)to[nw][j]=to[o][j];
fail[o]=fail[nx]=nw;
for(;i&&to[i][w]==o;i=fail[i])to[i][w]=nw;
}
now=nx;
}
int a[1000005],b[2000005],mx[1000005];
void work(){
for(int i=0;i<=l;++i)a[i]=0;
for(int i=1;i<=t;++i)++a[len[i]],cnt[i]=is[i];
for(int i=1;i<=l;++i)a[i]+=a[i-1];
for(int i=t;i;--i){b[a[len[i]]]=i;--a[len[i]];}
for(int i=t;i;--i){
cnt[fail[b[i]]]+=cnt[b[i]];
chkmax(mx[len[b[i]]],cnt[b[i]]);
}
for(int i=l;i;--i)chkmax(mx[i-1],mx[i]);
for(int i=1;i<=l;++i)write(mx[i],'\n');
}
}
#### 求两个串的最长公共子串
对S
建后缀自动机。然后对T
的每个前缀求最长匹配的后缀长度。
假设已经求出了T
的答案要求出T+c
的答案。
设当前匹配到的状态是st
,匹配的长度是l
。
进行操作: 1.
若trans(st,c)
存在,则st=trans(st,c),l=l+1
。 2.
否则l=maxlen(link(st)),st=link(st)
,回到1
求S
有多少个子串和T
循环同构。
如果S
多组询问:
对T+T
建SAM,然后求出S
每个前缀S[1:i]
最长匹配后缀长度长度。若≥|T|
,则ans=ans+1
如果T
多组询问:
对S
建SAM,然后求出T+T
每个前缀匹配到的位置。
若匹配到某个前缀时st
满足maxlen(link(st))≥|T|
则st=link(st)
没有问题。若l≥|T|
,则若没到达过这个st
(就是这个串没出现过)则ans+=cnt(st)
(求出这个串在S
中出现次数)
int match(char *t){
++ts;
int at=1,s=0,ans=0;
int m=strlen(t);
for(char *c=t;*c;++c){
int w=*c-'a';
while(at && !to[at][w])at=fail[at];
if(!at){at=1;s=0;}
else{s=min(s,len[at])+1;at=to[at][w];}
}
for(char *c=t;*c;++c){
int w=*c-'a';
while(at && !to[at][w])at=fail[at];
if(!at){at=1;s=0;}
else{s=min(s,len[at])+1;at=to[at][w];}
while(len[fail[at]]>=m){at=fail[at];s=len[at];}
if(s>=m && tag[at]!=ts){
ans+=cnt[at];
tag[at]=ts;
}
}
return ans;
}