后缀自动机作为一种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
",ans);
}
signed main(){
n=gi();
for(R int i=1;i<=n;i++) v=gi(),SAMadd(v);
}
P4248 [AHOI2013]差异
求
很显然,( ext{len}(T_i)+ ext{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);
}