后缀自动机SAM学习笔记(应用)

后缀自动机

原理多而复杂,这里不叙述一遍了

dalao的视频和文章:图解视频 文字说明

两者配合食用效果极佳

应用

主要是来将一下应用的,只匹配子串的SAM题几乎没有,洛谷的模板题也是要经过处理才能实现的

先给个模板,用于判断子串是否存在

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int t[maxn][26];
int len[maxn],fa[maxn];
string s;
int lst=1,cnt=1;
void add(int c){
	int p=lst,u=++cnt;
	lst=u;
	len[u]=len[lst]+1;
	for(;p&&(!t[p][c]);p=fa[p]) t[p][c]=u;
	if(!p){
		fa[u]=1;
		return;
	}
	int q=t[p][c];
	if(len[q]==len[p]+1){
		fa[u]=q;
		return;
	}
	int v=++cnt;
	len[v]=len[p]+1;
	memcpy(t[v],t[q],sizeof(t[q]));
	fa[v]=fa[q],fa[u]=fa[q]=v;
	for(;p&&t[p][c]==q;p=fa[p]) t[p][c]=v;
	return;
}
bool query(string& s){
	int p=1;
	for(int i=0;i<s.size();i++){
		if(!t[p][s[i]-'a']) return 0;
		p=t[p][s[i]-'a'];
	}
	return 1;
} 
int main(){
	cin>>s;
	for(int i=0;i<s.size();i++) add(s[i]-'a');
	string in;
	while(true){
		cin>>in;
		if(query(in)) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
} 

洛谷P3804

如何计算子串的出现个数?

在parent tree上,每个节点都是其子节点的真后缀
所以若子节点出现了,该节点也必定会出现

在建立parent tree时,在添加新的字符后,将当前序列(自动机已处理过的所有字符组成的串)的出现个数+1
具体表现为建立一个数组,将序列所对的新节点u的出现次数+1

在程序中写为f[x]=1,这是因为每一个这样的序列都会有一个自己的新节点,所以至多只加这一次

然后你会发现,你维护了一个树上差分数组!!!

对parent tree建图,dfs将差分数组转为统计出现次数的数组
对于每一个节点,len[i]表示最长子串长度,要求最大值,可以忽略同一节点上的其他串
统计答案:if(f[i]!=1) ans=max(ans,1ll*len[i]*f[i])

CODE

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5; 
int t[maxn][26];
int fa[maxn],len[maxn],f[maxn];
int lst=1,tot=1;
int head[maxn],nxt[maxn<<1],e[maxn<<1];
int cnt;
string s;
void init_mp(){
	memset(head,-1,sizeof(head));
	cnt=0;
}
void add_edge(int u,int v){
	e[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
	return; 
}
void add(int c){
	int p=lst,u=++tot;
	lst=u,f[u]=1;
	len[u]=len[p]+1;
	for(;p&&(!t[p][c]);p=fa[p]) t[p][c]=u;
	if(!p){
		fa[u]=1;
		return;
	}
	int q=t[p][c];
	if(len[q]==len[p]+1){
		fa[u]=q;
		return;
	}
	int v=++tot;
	len[v]=len[p]+1;
	memcpy(t[v],t[q],sizeof(t[q]));
	fa[v]=fa[q],fa[u]=fa[q]=v;
	for(;p&&t[p][c]==q;p=fa[p]) t[p][c]=v;
	return;
}
void dfs(int x){
	for(int i=head[x];~i;i=nxt[i]){
		dfs(e[i]);
		f[x]+=f[e[i]];
	}
	return;
}
int main(){
	cin>>s;
	for(int i=0;i<s.size();i++) add(s[i]-'a');
	init_mp();
	for(int i=2;i<=tot;i++) add_edge(fa[i],i);
	dfs(1);
	long long ans=0;
	for(int i=2;i<=tot;i++){
		if(f[i]!=1) ans=max(ans,1ll*len[i]*f[i]);
	}
	printf("%lld",ans);
	return 0;
}

洛谷P5546

题目传送门

这类问题在SAM中有两种做法

1.狭义上 很高级的叫法,毕竟另外一种网上叫广义 ,只对一个字符串(通常选取最短的)建自动机
然后把其他串放到自动机上去匹配。这种方式比较繁琐,数组也很多很杂,这里就不讨论了
感兴趣可以参考dummyummy的博客

2.广义上,把所有字符串连起来,对整个字符串建机,统计出现个数,找到满足f[i]==n的最长子串

这样做几个问题:
1.拼接以后相邻两个字符串之间会产生新的子串,会干扰结果

在每个字符串后面添加特殊字符,这里千万别都用一个相同的,考虑0,1,0,1...交替使用
证明:如果用一个相同的,假设每个字符串都一样,那么这个字符就有可能会被计算到公共字串当中
使用01时,即使被计算进去,最后也会因为个数不达标被淘汰掉

2.同一字符串内有多个相同的子串,干扰统计结果

使用bool记录该子串在哪些字符串中出现过,这类题目字符串个数一般较小,可以使用状态压缩优化


字符串的统计和洛谷P3804一样,树上差分+建图+dfs即可

CODE

#include<bits/stdc++.h>
using namespace std;
const int maxn=10002;
int t[maxn<<1][28];
int len[maxn<<1],fa[maxn<<1];
int f[maxn<<1];
string s[5];
int n;
int lst=1,idx=1;
void add(int x,int id){
	int p=lst,u=++idx;
	lst=u;
	len[u]=len[p]+1;
	f[u]|=(1<<id);
	for(;p&&(!t[p][x]);p=fa[p]) t[p][x]=u;
	if(!p){
		fa[u]=1;
		return;
	}
	int q=t[p][x];
	if(len[q]==len[p]+1){
		fa[u]=q;
		return;
	}
	int v=++idx;
	len[v]=len[p]+1;
	memcpy(t[v],t[q],sizeof(t[q]));
	fa[v]=fa[q],fa[u]=fa[q]=v;
	for(;p&&t[p][x]==q;p=fa[p]) t[p][x]=v;
	return;
}
int head[maxn<<1],nxt[maxn<<1],e[maxn<<1];
int cnt;
void init_mp(){
	memset(head,-1,sizeof(head));
	cnt=-1;
	return;
}
void add_edge(int u,int v){
	e[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
	return;
}
void dfs(int u){
	for(int i=head[u];~i;i=nxt[i]){
		dfs(e[i]);
		f[u]|=f[e[i]];
	}
	return;
}
int main(){
	cin>>n;
	for(int i=0;i<n;i++) cin>>s[i];
	for(int i=0;i<n;i++){
		for(int j=0;j<s[i].size();j++){
			add(s[i][j]-'a',i);
		}
		add(26+i%2,i);
	}
	init_mp();
	for(int i=2;i<=idx;i++){
		add_edge(fa[i],i);
	}
	dfs(1);
	int ac=(1<<n)-1;
	int ans=0;
	for(int i=1;i<=idx;i++){
		if(f[i]==ac){
			ans=max(ans,len[i]);
		}
	}
	printf("%d",ans);
	return 0;
}

其他例题继续更新中

posted @ 2025-06-10 23:40  huangems  阅读(13)  评论(0)    收藏  举报