Loading

后缀自动机(SAM)

后缀自动机作为一种OI新兴的字符串处理工具,越来越...

打住你的论文行为

SAM的定义

观前提示:笔者是从2015年国集论文中学习的SAM

一个串 \(S\) 的后缀自动机是一个有限状态自动机(DFA)

它能且只能接受所有 \(S\) 的后缀,并且拥有最少的状态与转移

首先我们要插入SAM的串为 \(S\),长度为 \(|S|\)\(S_{l,r}\) 为第 \(l\) 个字符到第 \(r\) 个字符形成的字串

对于一个字串 \(t\)\(right_t\)\(S\) 中所有出现 \(t\) 的右端点

例如一个串 ababbab,字串 ab\(right\) 集合为 \(\{2,4,7\}\)

每个状态 \(s\) 代表了唯一的 \(right\) 集合

对于一个状态 \(s\),设 \(len_s\) 为其所有代表状态中最长串的长度

每个状态还有一个 \(fa\) 指针

SAM的构造

我们假设现在已经插入了前 \(|S|-1\) 个字符,现在要插入第 \(|S|\) 个字符,设这个字符是 #a(为了与平常的 a 区别,#a 代表一个字符)

看我暴力

我们很容易发现不能暴力加边转移,因为对于一个状态 \(s\),能从 \(1\) 状态到达 \(s\) 的串肯定是其后缀,那么我们暴力加转移边 #a,形成了个啥呢

例如 abab,要插入 c,变成 ababc

好,暴力,\(S_{1,3}\) 后面来个状态 c,形成了 abac!Wonderful Answer!

肯定是不行的,我们此时能插入 #a 的状态肯定代表的是串 \(S_{1,|S|-1}\) 的后缀,因为这样加转移边 #a 之后,跑出来的才是新串的后缀

现在介绍 \(fa\) 指针,从一个状态 \(s\) 跳到 \(fa_s\)\(fa_s\) 代表的是 \(s\) 的后缀

也就是说,跳 \(fa\) 相当于访问 \(s\) 的一个后缀

到此,我们发现了一个加边方法,从 \(|S|-1\) 不断跳 \(fa\) 然后加边,最后更新 \(|S|\)\(fa\)


但是,我们有时候跳到的状态已经有了一个向 #a 的转移边,此时不要以为直接结束就完事了,我们需要分类讨论

设此时的状态为 \(p\),沿着这条已有的转移边能走到的状态为 \(q\)

  • 如果 \(len_q=len_p+1\)

很简单吧,此时 \(q\)\(right\) 集合依然没有什么变化,令 \(fa=q\) 即可

  • 如果 \(len_q>len_p+1\)

此时 \(q\) 代表的串中,长度不超过 \(len_p+1\) 的串的 \(right\) 集合会多出来一个值 \(|S|\)(因为插入字符 #a 嘛,然后 \(p\) 有恰好有这个转移边),但是长度超过 \(len_p+1\) 的串的 \(right\) 集合却没有,一个状态不能同时代表两个不同的 \(right\) 集合,此时我们需要新建状态

新建状态就很简单啦,因为只有 \(right\) 集合不同,所以我们除了 \(right\) 集合改改,剩下的原样复制

此时你已经成功的构建了SAM

例题

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

题目链接

每个 \(s\) 状态代表了唯一的 \(right\) 集合

这题没有让我们求出子串具体是什么,所以直接对每个状态取最长的串即可

沿着 \(parent\) 树连边,之后跑一遍树形dp即可

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define N 2000001
#define INF 1100000000
#define Kafuu return
#define Chino 0
#define fx(l,n) inline l n
#define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
#define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
#define R register int
using namespace std;
string st;
int last=1,ndn=1,num,head[N],at[N],ans;
struct SAM{
	int c[26],len,fa;
}s[N];
struct Edge{
	int na,np;
}e[N<<1];
queue<int>q;
fx(void,add)(int f,int t){
	e[++num].na=head[f];
	e[num].np=t;
	head[f]=num;
}
fx(void,SAMadd)(const int val){
	int bf=last,now=++ndn;
	at[last=now]=1;
	s[now].len=s[bf].len+1;
	while(bf&&!s[bf].c[val]){
		s[bf].c[val]=now;
		bf=s[bf].fa;
	}
	if(!bf) s[now].fa=1;
	else{
		int to=s[bf].c[val];
		if(s[to].len==s[bf].len+1) s[now].fa=to;
		else{
			int nto=++ndn;
			s[nto]=s[to];
			s[nto].len=s[bf].len+1;
			s[to].fa=s[now].fa=nto;
			while(bf&&s[bf].c[val]==to){
				s[bf].c[val]=nto;
				bf=s[bf].fa;
			}
		}
	}
}
fx(void,dp)(const int now){
	for(R i=head[now];i;i=e[i].na){
		dp(e[i].np);
		at[now]+=at[e[i].np];
	}
	if(at[now]>1) ans=max(ans,at[now]*s[now].len);
}
signed main(){
	cin>>st;
	for(R i=0;i<st.length();i++) SAMadd(st[i]-'a');
	for(R i=2;i<=ndn;i++) add(s[i].fa,i);
	dp(1);
	cout<<ans;
	Kafuu Chino;
}

P2408 不同子串个数

题目链接

我们知道,每一个子串在SAM都可以被唯一的表示出来

那么从根节点向每个节点跑,跑出来的即是所有的子串

DAG上玩dp即可

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define N 2000001
#define INF 1100000000
#define Kafuu return
#define Chino 0
#define fx(l,n) inline l n
#define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
#define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
#define R register int
#define int long long
using namespace std;
string st;
int n,last=1,ndn=1,num,head[N],at[N],ans[N];
struct SAM{
	int c[26],len,fa;
}s[N];
struct Edge{
	int na,np;
}e[N<<1];
queue<int>q;
fx(void,add)(int f,int t){
	e[++num].na=head[f];
	e[num].np=t;
	head[f]=num;
}
fx(void,SAMadd)(const int val){
	int bf=last,now=++ndn;
	at[last=now]=1;
	s[now].len=s[bf].len+1;
	for(;bf&&!s[bf].c[val];bf=s[bf].fa) s[bf].c[val]=now;
	if(!bf) s[now].fa=1;
	else{
		int to=s[bf].c[val];
		if(s[to].len==s[bf].len+1) s[now].fa=to;
		else{
			
			int nto=++ndn;
			s[nto]=s[to];
			s[nto].len=s[bf].len+1;
			s[to].fa=s[now].fa=nto;
			for(;bf&&s[bf].c[val]==to;bf=s[bf].fa) s[bf].c[val]=nto;
		}
	}
}
fx(int,dfs)(const int now){
	if(ans[now]) return ans[now];
	for(R i=0;i<26;i++) if(s[now].c[i]) ans[now]+=dfs(s[now].c[i])+1;
	return ans[now];
}
signed main(){
	cin>>n>>st;
	for(R i=0;i<st.length();i++) SAMadd(st[i]-'a');
	cout<<dfs(1);
	Kafuu Chino;
}

P4070 [SDOI2016]生成魔咒

题目链接

由SAM性质可知,每个状态 \(s\) 代表的串的长度是 \((S_{1,fa_s},S_{1,s}]\)

由于SAM本来就是在线的,所以每次加点直接统计即可

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
#include<algorithm>
#include<queue>
#define N 1000001
#define M 5001
#define INF 1100000000
#define Kafuu return
#define Chino 0
#define fx(l,n) inline l n
#define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
#define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
#define int long long
#define R register
#define C const
using namespace std;
int last=1,now,ndn=1,top,ans,num,head[N],size[N],n,v;
string st;
struct SAM{
	int len,fa;
	map<int,int>c;
}s[N];
fx(int,gi)(){
	R char c=getchar();R int s=0,f=1;
	while(c>'9'||c<'0'){
		if(c=='-') f=-f;
		c=getchar();
	}
	while(c<='9'&&c>='0') s=(s<<3)+(s<<1)+(c-'0'),c=getchar();
	return s*f;
}
fx(void,SAMadd)(C int val){
	int bf=last,now=++ndn;last=now;
	size[now]=1;
	s[now].len=s[bf].len+1;
	while(bf&&!s[bf].c[val]){
		s[bf].c[val]=now;
		bf=s[bf].fa;
	}
	if(!bf) s[now].fa=1;
	else{
		int to=s[bf].c[val];
		if(s[to].len==s[bf].len+1) s[now].fa=to;
		else{
			int nq=++ndn;
			s[nq]=s[to];
			s[nq].len=s[bf].len+1;
			s[to].fa=s[now].fa=nq;
			while(bf&&s[bf].c[val]==to){
				s[bf].c[val]=nq;
				bf=s[bf].fa;
			}
		}
	}
	ans+=s[now].len-s[s[now].fa].len;
	printf("%lld\n",ans);
}
signed main(){
	n=gi();
	for(R int i=1;i<=n;i++) v=gi(),SAMadd(v);
}

P4248 [AHOI2013]差异

题目链接

\[\sum\limits_{1\le i<j\le n}\text{len}(T_i)+\text{len}(T_j)-2\times\text{lcp}(T_i,T_j) \]

很显然,\(\text{len}(T_i)+\text{len}(T_j)\) 是一个定值,故我们只需要求字符串两两的最长公共前缀的长度之和即可

由于这是个后缀自动机,公共前缀不好求,但是公共后缀却可以方便的求出来

我们将原字符串反过来建SAM即可

容易发现两个前缀的公共后缀就在 \(parent\) 树上它们的LCA那个状态上

此时本题就变成了统计一个点是多少点的LCA,然后答案累计起来

这是个简单问题,将叶节点置 \(1\),树形dp即可

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define N 1000001
#define M 5001
#define INF 1100000000
#define Kafuu return
#define Chino 0
#define fx(l,n) inline l n
#define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
#define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
#define int long long
#define R register
#define C const
using namespace std;
int last=1,now,ndn=1,top,ans,num,head[N],size[N],n;
string st;
struct SAM{
	int c[26],len,fa;
}s[N];
struct Edge{
	int na,np;
}e[N];
fx(void,add)(int f,int t){
	e[++num].na=head[f];
	e[num].np=t;
	head[f]=num;
}
fx(int,gi)(){
	R char c=getchar();R int s=0,f=1;
	while(c>'9'||c<'0'){
		if(c=='-') f=-f;
		c=getchar();
	}
	while(c<='9'&&c>='0') s=(s<<3)+(s<<1)+(c-'0'),c=getchar();
	return s*f;
}
fx(void,SAMadd)(C int val){
	int bf=last,now=++ndn;last=now;
	size[now]=1;
	s[now].len=s[bf].len+1;
	while(bf&&!s[bf].c[val]){
		s[bf].c[val]=now;
		bf=s[bf].fa;
	}
	if(!bf) s[now].fa=1;
	else{
		int to=s[bf].c[val];
		if(s[to].len==s[bf].len+1) s[now].fa=to;
		else{
			int nq=++ndn;
			s[nq]=s[to];
			s[nq].len=s[bf].len+1;
			s[to].fa=s[now].fa=nq;
			while(bf&&s[bf].c[val]==to){
				s[bf].c[val]=nq;
				bf=s[bf].fa;
			}
		}
	}
}
fx(void,tdp)(int now){
	for(R int i=head[now];i;i=e[i].na){
		tdp(e[i].np);
		ans+=size[now]*size[e[i].np]*s[now].len;
		size[now]+=size[e[i].np];
	}
}
signed main(){
	cin>>st;
	n=st.length();
	for(R int i=st.length()-1;~i;i--) SAMadd(st[i]-'a');
	for(R int i=2;i<=ndn;i++) add(s[i].fa,i);
	tdp(1);
	printf("%lld",(n-1)*n*(n+1)/2-2*ans);
}
posted @ 2021-03-09 20:16  zythonc  阅读(142)  评论(0编辑  收藏  举报