后缀自动机SAM


建议先学会 Trie 图和 AC 自动机 了解自动机大概是什么。

后缀自动机SAM

P3804 【模板】后缀自动机 (SAM)

请你求出 $S$ 的所有出现次数不为 $1$ 的子串的出现次数乘上该子串长度的最大值。

对于题目要求,我们首先要统计所有子串的出现次数,还要记录所有子串的长度。如果暴力枚举字符串复杂度为 $O(n^3)$($O(n^2)$ 的子串枚举,$O(n^2)$ 的子串每位判断),但是如果处理每个字符的新加入的情况,虽然复杂没变,但是如果放在字典树($Trie$ 树)上匹配时,是 $O(n^2)$ 的复杂度(叶子节点)。因为从整个字符串的判断,变成了点个字符的判断。

如果要继续优化,那么就要建 $Trie$ 图了。

这里为了更加直观的描述思路,我们讨论的是一个新考虑字符要经历什么。

(如果字体过小,请自行调整网页缩放。)

这里的 $f_a$ 的箭头不是正确的在这里只是解释新插入的点会经历什么,真正的 $f_a$ 会在后面介绍。

SAM-1

SAM-2

上面几个图片记录了三种情况:

  • 之前没出现过这种字符,直接更新到根节点。

  • 之前出现过这种字符,当时“冲突”地点和之前出现的相同字符的位置相邻。

  • 之前出现过这种字符,当时“冲突”地点和之前出现的相同字符的位置不相邻,开新点维护。注意新点的长度的初始化是根据“冲突”地点累计的。

$f_a$ 指向的是前一个相同字符的位置,如果没有就只会根节点。或者是开的新点就像上面最后几张图描述的。

一个串:

$aab$

可以在:

$aabaa$

$aabaabaa$

中存在(后面增加的是不断继续讨论新的字符)。

所以已经存在的一定存在(有点奇怪,但是没问题!)。

在我们建树的时候,会初始化长度,而新的字符在后面形成的短串,其长串一定包含。

那么就可以通过找父节点统计出现的次数,根据字串长度,求权值。

一般情况下,SAM 当成模板背,用的时候直接用即可。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<vector>
#include<random>
#include<ctime>
using namespace std;
long long r_r(){//快读 
	long long k=0,f=1;
	char c=getchar();
	while(!isdigit(c)){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(isdigit(c)){
		k=(k<<1)+(k<<3)+(c^48);
		c=getchar();
	}
	return k*f;
} 
const int o_o=3e6+10;
char s_s[o_o];//原字符串 
long long a_s;//结果统计 
int m_p[o_o][30];//存 trie 树 
int f_a[o_o];//父节点 
int k_k=1;
int s_n=1;//节点编号 
int e_d[o_o];//结束长度 
int n_m[o_o];//子树大小 
struct po{
	int v;
	int n_t;
}p_p[o_o];
int h_d[o_o];
int x_p;
void a_d(int u,int v){//加边 
	p_p[++x_p].v=v;
	p_p[x_p].n_t=h_d[u];
	h_d[u]=x_p;
}
void i_n(int x,int i){
    int l_t=k_k;//记录上一个字符的位置 
	n_m[k_k=++s_n]=1;//初始化新节点次数 
	e_d[k_k]=i;//新字符串结尾编号
    while(l_t&&m_p[l_t][x]==0){//存在上一个字符,上一个字符没有连接当前字符 
		m_p[l_t][x]=k_k;//连接字符 
		l_t=f_a[l_t];//爬祖先,回找 
	}
    if(l_t==0){//达到根节点 
		f_a[k_k]=1;//特判设置父节点 
		return;
	}
	int n_w=m_p[l_t][x];//记录当前位置的字符 
    if(e_d[l_t]+1==e_d[n_w]){//如果两个节点是相邻的 
		f_a[k_k]=n_w;//新节点的父亲为当前位置的字符 
		return; 
	}
    e_d[++s_n]=e_d[l_t]+1;//记录结束编号 
    for(int i=1;i<=26;i++)m_p[s_n][i]=m_p[n_w][i];//继承节点儿子 
    f_a[s_n]=f_a[n_w];//继承父节点 
	f_a[n_w]=s_n;//更新父节点 
	f_a[k_k]=s_n;//更新父节点 
    for(int i=l_t;m_p[i][x]==n_w;i=f_a[i])m_p[i][x]=s_n;//更新节点 
}
void d_f(int k){
    for(int i=h_d[k];i;i=p_p[i].n_t){
    	int v=p_p[i].v;
		d_f(v);
		n_m[k]+=n_m[v];//累计每个节点的出现次数 
	}
    if(n_m[k]!=1)a_s=max(a_s,(long long)n_m[k]*e_d[k]);//取最大值 
}
int l_s=0;
int main(){
    scanf("%s",s_s+1);
    l_s=strlen(s_s+1);
	for(int i=1;i<=l_s;i++)i_n(s_s[i]-'a'+1,i);
    for(int i=2;i<=s_n;i++)a_d(f_a[i],i);//父节点和节点之间加边 
	d_f(1);
	printf("%lld",a_s);//输出答案 
	return 0;
}

统计字符串不同字串

P2408 不同子串个数

SAM 建出后,深搜贡献累计即可(相同的子串都已经合并了)。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>
#include<string>
#include<cmath>
#include<map>
#include<unordered_map>
using namespace std;
long long r_r(){
	long long x=0,f=1;
	char c=getchar();
	while(!isdigit(c)){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(isdigit(c)){
		x=(x<<1)+(x<<3)+(c^48);
		c=getchar();
	}
	return x*f;
}
const long long o_o=1e6+10;
string s;
int k_k=1,c_n=1;
int n_m[o_o];
int m_p[o_o][31];
int f_a[o_o];
int l_n[o_o];
void i_n(int x,int i){
	int l_t=k_k;
	n_m[k_k=++c_n]=1;
	l_n[k_k]=i;
	while(l_t&&m_p[l_t][x]==0){
		m_p[l_t][x]=k_k;
		l_t=f_a[l_t];
	}
	if(!l_t){
		f_a[k_k]=1;
		return ;
	}
	int n_w=m_p[l_t][x];
	if(l_n[l_t]+1==l_n[n_w]){
		f_a[k_k]=n_w;
		return ;
	}
	l_n[++c_n]=l_n[l_t]+1;
	for(int j=1;j<=26;j++)m_p[c_n][j]=m_p[n_w][j];
	f_a[c_n]=f_a[n_w];
	f_a[n_w]=f_a[k_k]=c_n;
	int k=l_t;
	while(m_p[k][x]==n_w){
		m_p[k][x]=c_n;
		k=f_a[k];
	}
}
long long a_s[o_o];
long long d_f(int k){//统计,不同字串个数 
	if(a_s[k])return a_s[k];
	for(int i=1;i<=26;i++)
		if(m_p[k][i])a_s[k]+=d_f(m_p[k][i])+1;//记录并继续深搜 
	return a_s[k];//返回结果 
}
int main(){
	char c=getchar();
	while(c>'z'||c<'a')c=getchar();
	int i_i=0;
	while(c>='a'&&c<='z'){
		i_n(c-'a'+1,++i_i);
		c=getchar();
	}
	printf("%lld",d_f(1));//输出结果 
	return 0;
}

文章作者: 王大神——A001
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王大神——A001 !
  目录