【学习笔记】后缀自动机 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。
如果 maxlen(p)+1=maxlen(q) ,则将cur的link指向q,结束。
否则,考虑q的endpos集合的部分后缀并不是cur集合中的串的后缀。
此时我们需要新建一个辅助节点clone,复制q的所有信息,并将clone的长度改为maxlen(p)+1,再将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相同的一个集合,串的长度是连续的,且区间在[maxlen(link[now])+1,maxlen(now)],其中的短串为长串的后缀。
从根节点出发能走出所有的子串,走后缀链接能走到所有为当前点后缀的点。
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
}
posted @   flywatre  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示