后缀自动机(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]差异
求
很显然,\(\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);
}