后缀自动机的一些应用
未学习后缀自动机的话,可以去这里看一下
由于字符串变化多端,这里介绍一些的简单应用,以增加一些理解;
后缀自动机的应用
本质不同子串的个数
我们有数组的做法,在上也一样可做;
发现其实上是没有重复子串的,我们只需要统计出上所有的子串就可以了;
即
统计子串出现次数
求出上每个每个字串的出现次数;
其实就是求每个状态的集合大小,怎么求呢?
首先每次新加的一个状态至少有一;
另外的,对于一个状态,每一个入边,都会对此状态产生贡献;
即
这样就可以在所构成的上跑拓扑;
两个串的最长公共子串
过不了的那种,可以先在第一个串上建,因为所有子串都会在上体现,我们可以考虑将第二个串在这个进行匹配;
算法流程:
当前状态(初始为),待匹配的字符是,已匹配的长度为
- 如果,意味着有这个子串,将,转移到下一个状态;
- 否则沿着向前寻找:
- 如果能找到一个状态,满足,则可以将,转移到;
- 否则;
- 更新答案;
时间复杂度是,是两个字符串的总长;
部分代码
void search(int n)
{
int p=1;
for(int i=1;i<=n;i++)
{
int c=s[i]-'a';
if(trans[p][c]) len++,p=trans[p][c];
else
{
for(;p&&!trans[p][c];p=link[p]);
if(p) len=maxlen[p]+1,p=trans[p][c];
else len=0,p=1;
}
ans=max(ans,len);
}
}
多个串的最长公共子串
假设有个长度为的字符串;
介绍两种方法
1 O(nk^2)
类似于统计子串出现次数,对于加进去的第个字符串,对每个状态的第维打上标记,即;
再用拓扑或深搜统计出每个状态在每个字符串里出现的次数;
如果存在一个状态,它的每一位都有值(即在每个字符串中都出现过),可以用它更新答案;
时间复杂度是,过不了那个例题,但很好理解;
深搜时的代码
inline void dfs(int x)
{
for(int i=head[x];i;i=a[i].nxt)
{
dfs(a[i].to);
for(int j=0;j<t;j++)//t是个数
T.num[x][j]+=T.num[a[i].to][j];
}
bool fl=0;
for(int j=0;j<t;j++)
if(!T.num[x][j])
{
fl=1;
break;
}
if(!fl) ans=max(ans,T.ml[x]);
}
2 O(nk)
另一种
由求两个串的情况拓展而来;
考虑在第一个串上建,把剩余的每个串都拿到上去匹配一下;
每次匹配时,得到当前匹配,每个状态的最长匹配成功长度;
按照理想的情况,最后在每次匹配的最长长度中找一个最小值,就是某一状态的所有匹配的最长公共匹配成功长度;
但是会存在一个问题,如果状态匹配成功了,其也一定存在的匹配,我们没有到达过,这一部分值就无从存在了,所以我们必须要更新;
但由于自身长度的限制,我们应当把;
(就是状态的当前匹配的最长匹配成功长度)
为了保证这个过程更新完全,我们需要按拓扑序来更新,即先更新了后才能更新;
我们发现,所以按照的排名更新就可以了,可以省去一次拓扑排序;
这样时间是
有关代码
//这都在SAM结构体中
void topu()
{
for(int i=1;i<=sz;i++) tub[ml[i]]++;
for(int i=1;i<=sz;i++) tub[i]+=tub[i-1];
for(int i=sz;i>=1;i--) b[tub[ml[i]]--]=i;
}
//类似于后缀数组,用一次基数排序得到拓扑序
void work()
{
int l=0,p=1;
for(int i=1;i<=n;i++)
{
int x=c[i]-'a';
if(trans[p][x]) l++,p=trans[p][x];
else
{
for(;p&&!trans[p][x];p=link[p]);
if(p) l=ml[p]+1,p=trans[p][x];
else l=0,p=1;
}
mas[p]=max(mas[p],l);
}
for(int i=sz;i>=1;i--)
{
int x=b[i],li=link[x];
mas[li]=max(mas[li],min(mas[x],ml[li]));
mis[x]=min(mis[x],mas[x]);
mas[x]=0;
}
}
字典序第k大子串
有两种情况
当不合并本质相同的串时:
还是基于一个思想,上的路径对应了一个一个的子串;
求第大子串就是求第大路径;
类似于二叉查找树的方法,如果我们能知道每个状态后连接了多少条路径的话,我们就可以把这个问题当成多叉查找树来做;
另外还需要考虑一个问题;
每个状态有多个,这相当与一个状态的副本数;
我们仍然记为;
将第个状态的路径总数,扩展为经过这个状态的子串数,记为;
因为,状态自身就存在个子串,且排在后面继续匹配的得到的子串的前面;
可以得到
另一种情况:
合并本质相同的串;
其实就是把每个状态(除了起始状态)的都置为;
#include<bits/stdc++.h>
#define ll long long
#define mp make_pair
using namespace std;
const int N=500010;
char c[N];
int n,ty,k;
struct SAM
{
int sz,last;
int tub[N<<1],b[N<<1];
int num[N<<1],sum[N<<1];
int link[N<<1],trans[N<<1][26],ml[N<<1];
SAM(){sz=last=1;}
void add(int id)
{
int x=++sz,p;
ml[x]=ml[last]+1;
num[x]=1;
for(p=last;p&&!trans[p][id];p=link[p]) trans[p][id]=x;
if(!p) link[x]=1;
else
{
int q=trans[p][id];
if(ml[q]==ml[p]+1) link[x]=q;
else
{
int y=++sz;
ml[y]=ml[p]+1;
memcpy(trans[y],trans[q],sizeof trans[y]);
link[y]=link[q];
for(;p&&trans[p][id]==q;p=link[p]) trans[p][id]=y;
link[x]=link[q]=y;
}
}
last=x;
}
void topu()
{
for(int i=1;i<=sz;i++) tub[ml[i]]++;
for(int i=1;i<=sz;i++) tub[i]+=tub[i-1];
for(int i=sz;i>=1;i--) b[tub[ml[i]]--]=i;
}
void getsz()
{
for(int i=sz;i>=1;i--)
{
int x=b[i];
sum[x]+=num[x];
num[link[x]]+=num[x];
}
if(ty==0) for(int i=1;i<=sz;i++) sum[i]=num[i]=1;
}
void getsum()
{
num[1]=sum[1]=0;
for(int i=sz;i>=1;i--)
{
int x=b[i];
for(int j=0;j<26;j++)
if(trans[x][j]) sum[x]+=sum[trans[x][j]];
}
}
}T;
inline void sol(int x,int k)
{
if(k<=T.num[x]) return ;
k-=T.num[x];
for(int i=0;i<26;i++)
{
int y=T.trans[x][i];
if(!y) continue;
if(k>T.sum[y])
{
k-=T.sum[y];
}
else
{
printf("%c",'a'+i);
sol(y,k);
break;
}
}
}
int main()
{
scanf("%s",c+1);
scanf("%d%d",&ty,&k);
n=strlen(c+1);
for(int i=1;i<=n;i++)
T.add(c[i]-'a');
T.topu();
T.getsz();
T.getsum();
if(T.sum[1]<k) printf("-1");
else sol(1,k);
return 0;
}
长度为k的子串的最大出现次数
如果只有一组询问,可以直接遍历,如果一个状态的,就可以用这个状态的更新;
如果有多组询问,可以将每个状态的,来对应更新;
再用来补全答案;
因为的后缀的出现次数一定大于的出现次数;
所有前缀的最长公共后缀和
先说求所有前缀的公共后缀:
先把我们得到的建成一棵树;
两个前缀所在的状态在这棵树上的的就是这两个前缀的最长公共后缀;(因为这两个前缀共有同样的后缀连接,它们后缀“断掉”的情况是相同的);
如何求所有呢?是不是求每两个前缀的最长公共后缀再乘上,它们出现次数的积就可以了呢?
不只是这样的,对于那些从初始状态出发,不是前缀的状态(一定是分裂出的状态),它们的出现次数是从它们被的前缀得来的,也就是说,它们的出现也伴随着前缀的出现,它们的最长公共后缀就等于它们所对应的前缀的最长公共后缀;
对于这部分的状态,也应该像上面那样处理;
所以我们必须求出
就是一个状态的;
这样时间复杂度是
我们考虑算每个状态的贡献,计算它是多少点对的;
拓扑之后
可以得到这样的代码
ll sol()
{
ll res=0;
for(int i=sz;i>=1;i--)
{
int x=b[i];//拓扑序对应的节点
res+=(ll)num[link[x]]*num[x]*ml[link[x]];
num[link[x]]+=num[x];
}
return res;
}
循环中的是未加入的值,相当不包含当前子树的其他子树的大小,可以与当前子树中节点一一构成一个节点对;
这样就可以得到答案;
但例题中好像要求所有后缀的最长公共前缀;
其实把最初的字符串翻转,这两者就是相等的了;
拓展:广义后缀自动机简介
上面有些题目是对一组字符串进行处理;
我们有的是对一个串建机,其他跑匹配,有的用特殊字符隔开在复合串上建机;
但这都有局限性;
而广义是专门来解决多字符串的;
这个上有所有字符串的子串,同样不重复;
构造的代码如下
void add(int id)
{
if(trans[last][id]&&ml[trans[last][id]]==ml[last]+1)//changed
{
last=trans[last][id];
return ;
}
int x=++sz,p,y;
bool fl=0;
ml[x]=ml[last]+1;
for(p=last;p&&!trans[p][id];p=link[p]) trans[p][id]=x;
if(!p) link[x]=1;
else
{
int q=trans[p][id];
if(ml[q]==ml[p]+1) link[x]=q;
else
{
if(ml[p]+1==ml[x]) fl=1;//changed
y=++sz;
ml[y]=ml[p]+1;
memcpy(trans[y],trans[q],sizeof trans[y]);
link[y]=link[q];
for(;p&&trans[p][id]==q;p=link[p]) trans[p][id]=y;
link[x]=link[q]=y;
}
}
if(fl==1) last=y;
else last=x;
}
需要注意的是,每次新加进一个串时,要把;
与普通只多了两个特判;
两个地方都是为了不构建一些不用的节点,第一个地方是利用已经建好了的点,第二个地方是跳过了空节点
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 对象命名为何需要避免'-er'和'-or'后缀
· SQL Server如何跟踪自动统计信息更新?
· AI与.NET技术实操系列:使用Catalyst进行自然语言处理
· 分享一个我遇到过的“量子力学”级别的BUG。
· Linux系列:如何调试 malloc 的底层源码
· 对象命名为何需要避免'-er'和'-or'后缀
· JDK 24 发布,新特性解读!
· C# 中比较实用的关键字,基础高频面试题!
· .NET 10 Preview 2 增强了 Blazor 和.NET MAUI
· SQL Server如何跟踪自动统计信息更新?