广义后缀自动机学习笔记
广义后缀自动机学习笔记
前言
为了方便,下文有如下约定:
-
在下文中,广义后缀自动机简称广义
。 -
记
为字符串 的长度。 -
记
为字符集, 为字符集大小。 -
在针对时间复杂度的分析时,
指 树的结点数, 指字符串总长。
前置知识
- 后缀自动机
概述
相比于
大部分能够使用普通
在有些题目中,会直接给出一个结点数为
定义与概念的拓展
因为将单串的
后缀
首先定义
结束位置
在新的后缀的定义下,一个字符串
后缀链接
在新的
根据这些新的定义对
伪广义
- 将所有字符串用不同的特殊字符分开后,对于合并后的整串构建
。 - 每次添加新串前将
改为 ,在原有 的基础上继续构建。
这两种都是常见的伪广义
合并法
这种写法不能保证线性的时间复杂度。
对于字符串个数无法保证的题目时,由于字符串之间的特殊符号的不重复,导致
归零法
这种写法有正确的时间复杂度,但会出现空结点问题。
当
这种问题在匹配子串时不会出现问题,但是它违反了
综上所述,伪广义
构建方法
离线构建
离线构建指的是先读入所有的字符串并建立出
构建
完成一棵
struct TRIE{
static const int N=1e6+5,S=1e6+5;
int n,tot;
char s[N];
struct Node{
int faz,ch;//父亲结点和对应字符
int son[26];
}p[S];
void Init(){//初始化
for(int i=0;i<=tot;i++){
p[i].faz=p[i].ch=0;
memset(p[i].son,0,sizeof p[i].son);
}
tot=0;
return ;
}
void Insert(char *s){//插入字符串
int now=0;
for(int i=0;s[i];i++){
int ch=s[i]-'a';
if(p[now].son[ch]==0){
p[now].son[ch]=++tot;
p[tot].faz=now;
p[tot].ch=ch;
}
now=p[now].son[ch];
}
return ;
}
void Build(){
scanf("%d",&n);
Init();
for(int i=1;i<=n;i++){
scanf("%s",s);
Insert(s);
}
return ;
}
}trie;
的构建
考虑在
首先考虑为什么这样子做对答案没有影响。在
时间复杂度是
struct SAM{
static const int N=1e6+5;
int tot;
int pos[N<<1];//pos 数组
long long ans;
struct Node{
int len,link;
int son[26];
}p[N<<1];
void Init(){//初始化
for(int i=0;i<tot;i++){
p[i].len=p[i].link=pos[i]=0;
memset(p[i].son,0,sizeof p[i].son);
}
tot=0;
p[0].len=0;
p[0].link=-1;
return ;
}
int Insert(int ch,int lst){//插入结点
p[++tot].len=p[lst].len+1;
int pos=lst;
while(pos!=-1&&p[pos].son[ch]==0){
p[pos].son[ch]=tot;
pos=p[pos].link;
}
lst=tot;
if(pos==-1)p[tot].link=0;
else{
int u=pos,v=p[pos].son[ch];
if(p[u].len+1==p[v].len)p[tot].link=v;
else{
p[++tot]=p[v];
p[tot].len=p[u].len+1;
p[v].link=p[tot-1].link=tot;
while(pos!=-1&&p[pos].son[ch]==v){
p[pos].son[ch]=tot;
pos=p[pos].link;
}
}
}
ans+=p[lst].len-p[p[lst].link].len;
return lst;
}
void Bfs(){
queue<int>q;
for(int i=0;i<26;i++){
if(trie.p[0].son[i]!=0)q.push(trie.p[0].son[i]);
}
pos[0]=0;
while(!q.empty()){
int now=q.front();
q.pop();
pos[now]=Insert(trie.p[now].ch,pos[trie.p[now].faz]);//存储对应的 pos 位置
for(int i=0;i<26;i++){
if(trie.p[now].son[i])q.push(trie.p[now].son[i]);
}
}
return ;
}
void Build(){
trie.Build();
Init();
Bfs();
printf("%lld\n%d",ans,tot+1);
return ;
}
}sam;
的构建
struct SAM{
static const int N=1e6+5;
int tot;
int pos[N<<1];
long long ans;
struct Node{
int len,link;
int son[26];
}p[N<<1];
void Init(){
for(int i=0;i<=tot;i++){
p[i].len=p[i].link=pos[i]=0;
memset(p[i].son,0,sizeof p[i].son);
}
tot=0;
p[0].len=0;
p[0].link=-1;
return ;
}
int Insert(int ch,int lst){
if(p[lst].son[ch]!=0&&p[p[lst].son[ch]].len==p[lst].len+1)return p[lst].son[ch];
//如果 lst 已经有标记为 c 的边,并且新插入的结点的 link 应该指向这条边对应的结点,那么新结点可以认为就是对应变指向的结点。
p[++tot].len=p[lst].len+1;
int pos=lst;
while(pos!=-1&&p[pos].son[ch]==0){
p[pos].son[ch]=tot;
pos=p[pos].link;
}
int Lst=lst,u,v;
lst=tot;
if(pos==-1)p[tot].link=0;
else{
u=pos,v=p[pos].son[ch];
if(u==Lst)lst=tot--;
//如果 lst 已经有标记为 c 的边,但是新插入的结点的 link 应该是分裂后的其中的一个结点。那么就说明需要分裂,新结点可以认为是复制出的结点。
if(p[u].len+1==p[v].len)p[tot].link=v;
else{
p[++tot]=p[v];
p[tot].len=p[u].len+1;
p[v].link=tot;
if(tot!=lst)p[tot-1].link=tot;
while(pos!=-1&&p[pos].son[ch]==v){
p[pos].son[ch]=tot;
pos=p[pos].link;
}
}
}
if(u!=Lst)ans+=p[lst].len-p[p[lst].link].len;
//如果结点不是新建的,那么之前的贡献已经统计过了,就不需要再统计了。
return lst;
}
void Dfs(int now){
for(int ch=0;ch<26;ch++){
if(trie.p[now].son[ch]!=0){
pos[trie.p[now].son[ch]]=Insert(ch,pos[now]);
Dfs(trie.p[now].son[ch]);
}
}
return ;
}
void Build(){
trie.Build();
Init();
Dfs(0);
printf("%lld\n%d",ans,tot+1);
return ;
}
}sam;
事实上,这样子的写法的复杂度相当于每一次将
在线构建
在线构建就是不预先建出
struct SAM{
static const int N=1e6+5;
int n,tot,lst;
int pos[N<<1];
long long ans;
char s[N];
struct Node{
int len,link;
int son[26];
}p[N<<1];
void Init(){
for(int i=0;i<=tot;i++){
p[i].len=p[i].link=pos[i]=0;
memset(p[i].son,0,sizeof p[i].son);
}
tot=0;
p[0].len=0;
p[0].link=-1;
return ;
}
void Insert(int ch){
if(p[lst].son[ch]!=0&&p[p[lst].son[ch]].len==p[lst].len+1){
lst=p[lst].son[ch];
return ;
}
int pos=lst;
p[++tot].len=p[lst].len+1;
while(pos!=-1&&p[pos].son[ch]==0){
p[pos].son[ch]=tot;
pos=p[pos].link;
}
int Lst=lst,u,v;
lst=tot;
if(pos==-1)p[tot].link=0;
else{
u=pos,v=p[pos].son[ch];
if(u==Lst)lst=tot--;
if(p[u].len+1==p[v].len)p[tot].link=v;
else{
p[++tot]=p[v];
p[tot].len=p[u].len+1;
p[v].link=tot;
if(tot!=lst)p[tot-1].link=tot;
while(pos!=-1&&p[pos].son[ch]==v){
p[pos].son[ch]=tot;
pos=p[pos].link;
}
}
}
if(u!=Lst)ans+=p[lst].len-p[p[lst].link].len;
return ;
}
void Build(){
Init();
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s);
lst=0;
for(int j=0;s[j];j++)Insert(s[j]-'a');
}
printf("%lld\n%d",ans,tot+1);
return ;
}
}sam;
特殊应用
子串出现次数
同样存储一个计数数组。
只需要在每一个字符串的每一个前缀所在的结点上将计数数组的值加
最长公共子串
考虑和求解子串出现次数相同的思路,通过计数数组存储每一个结点的字符串在哪些字符串中出现过。然后在后缀树上不断利用子结点更新父结点的计数数组。如果一个结点的计数数组中存储了所有的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?