【学习笔记】后缀自动机 SAM
由于本人时间原因,此处只为一个SAM的总结,讨论SAM的基本操作以及性质,详细证明
如要详细学习请查询luogu题解。
算法原理
SAM中每一个节点代表所有结束位置(endpos)相同的串的集合。
每个节点有:1.后缀链接link(到endpos包含它且maxlen最长的那个点,且是为当前点的后缀的点) 2.此点所代表的最长的串的长度(maxlen) 3.走字符 [a-z] 能走到的点(nex[a-z])
SAM的构造:
设上一个节点为last,当前开一个新节点为cur,当前插入的字符为c。
从last一直走它的link直到走到根节点或某点c的出边不为空,将路径上的所有点的c的出边设为cur。
如果走到根节点说明前面没有和它后缀相同的点,于是将cur的link指向根节点。
否则设走到的点为p,p点c出边指向的点为q。
如果 ,则将cur的link指向q,结束。
否则,考虑q的endpos集合的部分后缀并不是cur集合中的串的后缀。
此时我们需要新建一个辅助节点clone,复制q的所有信息,并将clone的长度改为,再将cur的link指向clone,q的link指向clone。
然而此时还有一个小问题,从cur的link走到的clone并不能由根节点走到clone所代表的endpos的集合,我们需要重定向一些边来使得此性质成立。
于是从p向上走link,将所有当前点c的出边指向q的边重定向到clone,走到第一个点c的出边不指向q的点即可退出。
考虑此时SAM的形态,q的分割为两个集合clone和q,由于clone的maxlen是小于q的,所以clone所代表的所有串是q的后缀。
性质
每个点所代表的是endpos相同的一个集合,串的长度是连续的,且区间在,其中的短串为长串的后缀。
从根节点出发能走出所有的子串,走后缀链接能走到所有为当前点后缀的点。
link构成的树是后缀树。
待补
code
talk is cheap,show me the code.
一些细节在代码中才能体现出来。
P3804 【模板】后缀自动机 (SAM)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int rd(){
int f=1,j=0;
char w=getchar();
while(!isdigit(w)){
if(w=='-')f=-1;
w=getchar();
}
while(isdigit(w)){
j=j*10+w-'0';
w=getchar();
}
return f*j;
}
const int N=1000010,M=3000010;
char s[N];
int n,cnt;
struct node{
int lin,to[26],len,siz;
}t[M];
int du[M],ans;
inline int insert(int last,int sumn){
int p=last,cur=++cnt;
t[cur].len=t[p].len+1,t[cur].siz=1;
while(p&&!t[p].to[sumn])t[p].to[sumn]=cur,p=t[p].lin;
if(!p)return t[cur].lin=1,cur;//注意设cur的lin为1
int q=t[p].to[sumn];
if(t[q].len==t[p].len+1)return t[cur].lin=q,cur;
int clone=++cnt;
t[clone]=t[q],t[clone].len=t[p].len+1,t[clone].siz=0;//辅助节点不参与后缀树的上出现位置个数统计
t[q].lin=t[cur].lin=clone;
while(p&&t[p].to[sumn]==q)t[p].to[sumn]=clone,p=t[p].lin;
return cur;
}
signed main(){
scanf("%s",s+1),n=strlen(s+1);
cnt=1;//从1开始是因为1为根可以判到从1出发的字符,以0为根要加特判
for(int i=1,last=0;i<=n;i++)last=insert(last,s[i]-'a');
for(int i=1;i<=cnt;i++)du[t[i].lin]++;
deque<int>que;
for(int i=1;i<=cnt;i++)if(du[i]==0)que.push_back(i);
while(!que.empty()){
int u=que.front();que.pop_front();
if(t[u].siz!=1)ans=max(ans,t[u].len*t[u].siz);
int x=t[u].lin;
du[x]--,t[x].siz+=t[u].siz;
if(!du[x])que.push_back(x);
}
printf("%lld\n",ans);
return 0;
}
广义sam
有两种构建方式
- 离线,bfs在字典树上建sam
- 在线,注意两个地方,见下代码
inline int insert(Re ch,Re last){//将ch[now]接到last后面
if(trans[last][ch]&&maxlen[last]+1==maxlen[trans[last][ch]])return trans[last][ch];
//已经存在需要的节点(特判1)
Re x,y,z=++O,p=last,flag=0;maxlen[z]=maxlen[last]+1;
while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
if(!p)link[z]=1;
else{
x=trans[p][ch];
if(maxlen[p]+1==maxlen[x])link[z]=x;
else{//需要拆分x,将len<=maxlen[p]+1的部分复制一个y出来
if(maxlen[p]+1==maxlen[z]/*或者写:p==last*/)flag=1;(特判2:此时的z会变成没有意义的节点)
y=++O;maxlen[y]=maxlen[p]+1;
for(Re i=0;i<C;++i)trans[y][i]=trans[x][i];
while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
link[y]=link[x],link[z]=link[x]=y;
}
}
return flag?y:z;//注意返回值
//返回值为:ch[now]插入到SAM中的节点编号,
//如果now不是某个字符串的最后一个字符,
//那么这次返回值将作为下一次插入时的last
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探