SAM 简记

SAM 可以表示一个字符串的所有子串。表示的意思是可以用从起点到某一点的路径表达。

SAM 是自动机,若它能表示一个字符串,则显然可以表示这个字符串的前缀。

所以,只要表示出所有的后缀,就能表示所有子串。

对于 s 的一个子串 tendpos(t) 表示其在 s 出现的结束位置集合。例如 s=abcbc 时,endpos(bc)={3,5}

显然,若两个子串的 endpos 有交,说明一个字符串出现的地方就有另一个字符串,所以其中一个是另一个的后缀,故两个 endpos 有包含关系。

所以,两个子串的 endpos 要么没有交,要么有包含关系。

endpos 相同的子串中,长度一定是一段连续的数字。因为其中最长的子串的 endpos 被比它短的包含,最短的子串的又包含所有比它长的子串,所以对于 endpos 相等的长度为 L1<L2 的两个子串,一定有长度为 L1+1,,L21 的子串的 endpos 与之相等。

我们可以把 endpos 相同的子串归为一类。在这一类中最长的字符串前面加一个字符,其 endpos 不同,但是被包含。加不同的字符可能会导出不同的 endpos,根据上面所述,它们没有交。所以可以通过在最长的字符前面加字符的方式拆分 endpos 集合。当然,不是所有集合中的数字都一定会保留下来。

所以我们可以看出一个树结构:根节点代表 endpos={1,2,,|s|} 的一类,这一类长度最小的是空串。然后可以通过向最长字符串前加字符的方式拆分成几个集合。我们叫这棵树 parent tree。

parent tree 的节点和 SAM 的节点意义一致(但是边不一致),所以我们可以把一个节点当做一个状态。其中 endpos 包含 |s| 的链上的点被称为终止状态,包含了 s 的后缀。在这棵树上从儿子到父亲的边被称为后缀链接,用 link(u) 表示,可以跳到自己的后缀。用 str(u) 表示状态 u 中的最长字符串,len(u)=|str(u)|

显然,状态 u 中最短字符串的长度为 len(link(u))+1

而 SAM 则用转移将点连接起来。转移是在 str(u)后面 添加字符得到的字符串对应的另一状态。

构造 SAM 的方式也是将字符一个个加入。对于每个状态,我们保存转移、linklen。初始时只有一个状态,编号为 0

int cur=++tot,u=lst;
len[cur]=len[lst]+1;lst=cur;
for(;~u&&!t[u][c];u=f[u])t[u][c]=cur;
if(u==-1)return f[cur]=0,void();

从原来表示整个字符串的状态开始,不断跳后缀链接,直到有 c 的转移。没有 c 的转移时,将其连接到 curcur 的意义是加入这个字符后表示整个字符串的状态。一旦有 c 转移,显然其后缀也必然有。f 表示后缀链接。若跳出去了,表明新字符串在老字符串中没有后缀,直接赋值为 0。否则,str(link(cur))=str(u)+c

int v=t[u][c];
if(len[u]+1==len[v])return f[cur]=v,void();

如果状态 v 恰好满足这个条件,说明 link(cur)=v

int p=++tot;len[p]=len[u]+1;f[p]=f[v];
memcpy(t[p],t[v],sizeof(t[p]));
for(;~u&&t[u][c]==v;u=f[u])t[u][c]=p;
f[v]=f[cur]=p;

否则必须将 v 拆成两个点,即将 str(u)+c 及其后缀分入 p,其他的留在 v,另一个点 p 复制 v 除了 len 的信息。造出了我们需要的 p 后,需要把 v 后面的所以最后将 u 及其后缀到 v 上的转移调整到 plink(v)=p

对于时间复杂度,我不懂。也许以后会补上。

P3804 【模板】后缀自动机 (SAM) 时还有个问题就是计算每个点的 endpos 的大小。可以把每个可能的元素即 1,2,,|s| 预先放入节点中。具体地,把每个前缀对应的状态的 size 加一,然后遍历 parent tree 得出大小。

#include<bits/stdc++.h>
using namespace std;
constexpr int N=2e6+5;
int tot,lst,f[N],t[N][26],len[N],siz[N];
vector<int>e[N];long long ans;
void dfs(int u){
	for(int v:e[u])dfs(v),siz[u]+=siz[v];
	if(u&&siz[u]>1)ans=max(ans,1ll*siz[u]*len[u]);
}
int main(){
	f[0]=-1;string s;cin>>s;
	for(int c:s){
		int cur=++tot,u=lst;c-='a';siz[cur]++;
		len[cur]=len[lst]+1;lst=cur;
		for(;~u&&!t[u][c];u=f[u])t[u][c]=cur;
		if(u==-1){f[cur]=0;continue;}
		int v=t[u][c];
		if(len[v]==len[u]+1)f[cur]=v;else{
			int p=++tot;len[p]=len[u]+1;f[p]=f[v];
			memcpy(t[p],t[v],sizeof(t[p]));
			for(;~u&&t[u][c]==v;u=f[u])t[u][c]=p;
			f[v]=f[cur]=p;
		}
	}
	for(int i=1;i<=tot;i++)e[f[i]].push_back(i);
	dfs(0);cout<<ans<<'\n';
	return 0;
}

SAM 可以理解为对一个字符串的所有子串建立的 ACAM。

posted @   hihihi198  阅读(59)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
主题色彩