后缀自动机&回文自动机学习笔记

时间:2022-11-10 17:56:09

在学了一天其实是边学边摆之后我终于大概$get$后缀自动机了,,,就很感动,于是时隔多年我终于决定再写篇学习笔记辽$QwQ$

$umm$和$FFT$学习笔记一样,这是一篇单纯的$gql$的知识总结博,对新手并不友好,想学$SAM$的话我是推荐几篇博客:1 2 3(没有$hihocoder$主要我$jio$得有点太理论化了,全是文字没有图其实我挺难看下去的然后也没那么形象比较难理解$kk$

然后因为我对纯文字的抽象知识点理解起来比较垃圾,,,所以全文可能会放比较多的图$QwQ$

先放个已经建好的$SAM$介绍下它的基本元素趴$QwQ$

后缀自动机&回文自动机学习笔记

这个是字符串$aabbabd$的$SAM$.不难发现这个图总的有三个基本元素,分别是节点.绿线.蓝线

事实上$SAM$的核心也就这三个东西,我大概分别介绍下这三个东西$QwQ$

首先是节点.

我们先定义一个东西,叫$endpos(x)$,表示的字符串$x$的所有出现位置,这里的出现位置指的末端的出现位置$QwQ$

就理解为,$kmp$的时候所有匹配到$x$的末尾节点的位置$QwQ$

然后对于一个节点的定义就是所有$endpos$相等的$x$的集合.

拿这个$aabbabd$举例?$\{aabb,abb,bb\}$的$endpos$就都等于$\{ 4 \}$,$\{b\}$的$endpos$就等于$\{3,4,6\}$

昂然后关于节点几个常见性质记录下,只是证明的话就懒得证了,其实大部分都还挺满显然的?不明白的自己举个例子啥的就$get$了$QwQ$

1.对于同一个节点内的所有字符串都互为后缀.

依然拿$aabbabd$举例子?看到这个$aabb,abb,bb$,发现,嗷,确实互为后缀呢$QwQ$

然后说了没有证明,不过挺显然的我$jio$得其实还,,,

2.设$x_1$,$x_2$都是$S$的子串,不妨设$|x_1|<|x_2|$,则有.$x_1$是$x_2$的后缀当且仅当$endpos(x_1) \supseteq endpos(s_2)$.$x_1$不是$x_2$的后缀当且仅当$endpos(s_1) \cap endpos(s_2) = \emptyset$

这个挺好理解我就不举例子辽$QwQ$

3.设$substrings(st)$表示节点$st$包含的所有字符串,$longest(st)$表示节点$st$所有字符串中最长的一个,$shortest(st)$表示节点$st$所有字符串中最短的一个.则有对所有$x\in substrings(st)$,都是$longest(st)$的后缀

$umm$这个其实和第一个是差不多的我不说了$QwQ$

4.对于任意一个状态$st$,以及$longest(st)$的任意一个后缀$x$,当$|shortest(st)|\leq |x|\leq longest(st)$时有$x\in substrings(st)$.也就说$substrings(st)$实际上是$longest(st)$的一段连续后缀$w$

证明显然不明白自己随便举例趴$QwQ$

5.没了,,,?$QwQ$

然后是这个绿色的线

在前面港节点的性质的时候就港了说$substrings(st)$就$longest(st)$的一段连续后缀,所以考虑为什么在$shortest(st)$后面就断开辣?

就依然拿前面那个$\{aabb,abb,bb\}$港趴,发现继续看后缀应该就是$b$了,但是发现,$b$的$endpos$是$\{3,4,6\}$,也是出现新节点.然后再后面后缀是$\_$,$endpos$是$\{1,2,3,4,5,6,7\}$,于是又出现了新节点

也就说当$longest(st)$的某个后缀在其他位置出现时,就会断掉,然后出现新的节点

在这个图上的话是有,节点$S$是$\_$,节点5是$b$,节点4是$\{aabb,abb,bb\}$,于是连一条$4->5->S$的路径,表示的$longest(4)$即$aabb$的所有后缀依次在$4,5,S$中$QwQ$

对了,叫绿线,听起来就,有点捞,,,还是说下鸭,官方名字叫$Suffix\ Link$

最后说下蓝色的线

首先说另外一个东西,设$next(st)$表示节点$st$遇到的下一个字母的集合,即$next(st)=\{S[i+1],i\in endpos(st)\}$

然后关于这个$next(st)$有一个性质是说,将节点$st$中的所有字符串后面接上字符$c$之后,都依然属于同一个节点

所以定义一个$trans(st,c)$表示在节点$st$后面接上$c$之后指向的节点.

然后这个蓝线就是这个$trans$,蓝线上面的字母就$c$.

最后关于构建我就不说了,,,主要原理我还没有完全$get$(就分类讨论三个里面最复杂的那个,,,我还没$get$昂$kk$

所以我就不说了,只大概说下,就对每个节点要保存的只有$longset,shortest,suffix\ link,trans$,然后构造是用的增量法,即将元素一个个加进去$QwQ$

然后放下板子题代码$QwQ$

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ll long long
#define gc getchar()
#define mp make_pair
#define P pair<int,int>
#define ri register int
#define rc register char
#define rb register bool
#define w(i) edge[i].wei
#define rp(i,x,y) for(ri i=x;i<=y;++i)
#define my(i,x,y) for(ri i=x;i>=y;--i)
#define lb(x) lower_bound(b+1,b+1+num,x)-b
#define ub(x) upper_bound(b+1,b+1+num,x)-b const int N=+;
int lst=,tot=,t[N],A[N],sz[N];
ll as;
char str[N];
struct node{int fa,len,to[];}nod[N<<]; il int read()
{
rc ch=gc;ri x=;rb y=;
while(ch!='-' && (ch>'' || ch<''))ch=gc;
if(ch=='-')ch=gc,y=;
while(ch>='' && ch<='')x=(x<<)+(x<<)+(ch^''),ch=gc;
return y?x:-x;
}
il void extend(ri c)
{
ri p=lst,np=++tot;lst=np;sz[np]=;nod[np].len=nod[p].len+;
while(p && !nod[p].to[c])nod[p].to[c]=np,p=nod[p].fa;
if(!p)return void(nod[np].fa=);
ri q=nod[p].to[c];if(nod[p].len+==nod[q].len)return void(nod[np].fa=q);
ri nq=++tot;nod[nq]=nod[q];nod[nq].len=nod[p].len+;nod[q].fa=nod[np].fa=nq;
while(p && nod[p].to[c]==q)nod[p].to[c]=nq,p=nod[p].fa;
} int main()
{
//freopen("3802.in","r",stdin);freopen("3802.out","w",stdout);
scanf("%s",str+);ri len=strlen(str+);rp(i,,len)extend(str[i]-'a'+);
rp(i,,tot)t[nod[i].len]++;rp(i,,tot)t[i]+=t[i-];rp(i,,tot)A[t[nod[i].len]--]=i;
my(i,tot,){ri nw=A[i];sz[nod[nw].fa]+=sz[nw];if(sz[nw]>)as=max(as,1ll*sz[nw]*nod[nw].len);}
printf("%lld\n",as);
return ;
}

阿最后还是写几个基本应用趴$QwQ$

拓扑序

根据$len$排序,得到的就是$parent$树的$bfs$序,也是自动机的拓扑序

rp(i,,tot)t[nod[i].len]++;rp(i,,tot)t[i]+=t[i-];rp(i,,tot)A[t[nod[i].len]--]=i;

判断子串/后缀

在$SAM$上匹配,如果没有转移到$NULL$就是子串,如果转移到接受状态就是后缀

顺便说下匹配?考虑对查询串一个个字母查询,类似于$AC$自动机和$kmp$一样的,一直跳$fa$直到到根或者可以匹配位置.然后如果能匹配就匹配,否则重置$t$和$p$,最后如果匹配完成就加入答案就成$QwQ$

不同子串个数

考虑对节点$x$,拥有的子串个数就$len[x]-len[fa[x]]$,然后$\sigema$下就成,$over$

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ll long long
#define gc getchar()
#define mp make_pair
#define P pair<int,int>
#define ri register int
#define rc register char
#define rb register bool
#define w(i) edge[i].wei
#define rp(i,x,y) for(ri i=x;i<=y;++i)
#define my(i,x,y) for(ri i=x;i>=y;--i)
#define lb(x) lower_bound(b+1,b+1+num,x)-b
#define ub(x) upper_bound(b+1,b+1+num,x)-b const int N=+;
int lst=,tot=,t[N],A[N],sz[N];
ll as;
char str[N];
struct node{int fa,len,to[];}nod[N<<]; il int read()
{
rc ch=gc;ri x=;rb y=;
while(ch!='-' && (ch>'' || ch<''))ch=gc;
if(ch=='-')ch=gc,y=;
while(ch>='' && ch<='')x=(x<<)+(x<<)+(ch^''),ch=gc;
return y?x:-x;
}
il void extend(ri c)
{
ri p=lst,np=++tot;lst=np;sz[np]=;nod[np].len=nod[p].len+;
while(p && !nod[p].to[c])nod[p].to[c]=np,p=nod[p].fa;
if(!p)return void(nod[np].fa=);
ri q=nod[p].to[c];if(nod[p].len+==nod[q].len)return void(nod[np].fa=q);
ri nq=++tot;nod[nq]=nod[q];nod[nq].len=nod[p].len+;nod[q].fa=nod[np].fa=nq;
while(p && nod[p].to[c]==q)nod[p].to[c]=nq,p=nod[p].fa;
} int main()
{
//freopen("3802.in","r",stdin);freopen("3802.out","w",stdout);
ri len=read();scanf("%s",str+);rp(i,,len)extend(str[i]-'a'+);
rp(i,,tot)as+=nod[i].len-nod[nod[i].fa].len;
printf("%lld\n",as);
return ;
}

求原串所有子串中字典序第$i$大

先求出拓扑序,从而求出每个节点的$endpos$数量$QwQ$

然后就按着拓扑序求出每个节点的子树大小,然后就用逼近法就成$QwQ$

求两个串的最长公共子串

先给第一个串建一个$SAM$,然后匹配第二个串就成,$over$

字符串最小表示法

考虑先复制字符串并丢进$SAM$中,然后每次贪心走就行$over$

求两个前缀的最长公共后缀

$umm$答案就$len[lca(x,y)]$?过分显然了$QwQ$

(这个可以用在两个串求任意两点结尾的$lcs$,就接在一起中间加"#"就变成这个了$QwQ$

求任意子串出现个数

考虑一个子串在它及它的子树中出现?所以按拓扑序$dp$就完事$QwQ$

求模式串在文本串中的出现位置

预处理一个$parent$数的倍增和$endpos$大小($umm$这个其实直接$len[x]-len[fa[x]]$就是,,,?

然后扫描模式串,记录当前匹配到了第多少位,然后判断是否匹配完了.如果匹配完了就倍增往上跳到满足$len\geq$的最高位置,把这个位置的$endpos$集合大小统计到答案中

最后

众所周知会$SAM$就会$PAM$了

所以我顺便写下$PAM$的学习笔记趴$QwQ$

$umm$其实我$jio$得这和$SAM$没有太大关系,,,?比较像$kmp$昂$QwQ$,,,?

咕了明天写$QwQ$