子序列自动机
前言
不是很难理解,简单地写一下,并不严谨。
内容来自 博客
构建
字符串 \(S\) ,长度为 \(n\),从 \(1\) 开始编号。
维护指针 \(to_{i,c}(i\in[0,n])\),表示 \(S\) 的第 \(i\) 个位置后,第一个字符 \(c\) 所处的位置,如果 \(i\) 位置后没有 \(c\),了,默认指向 \(n+1\) 。
其中 \(to_0,c\) 可表示 \(S\) 中第一个出现 \(c\) 的位置。
一般情况
根据指针 \(to\) 的定义容易得到构建方式:
for(int i=0;i<26;++i)las[i]=n+1;
for(int i=n;i>=0;--i){
for(int j=0;j<26;++j)to[i][j]=las[j];
if(i)las[s[i]-'a']=i;
}
其中 \(las_c\) 表示目前所找到的字符 \(c\) 的下标最小的位置。
时间复杂度 \(O(n|\sum|)\)。\(|\sum|\) 为字符集大小。
查询
假设现在需要查询 \(t\) 是否为 \(s\) 的一个子序列,只需要从 \(0\) 号点开始,沿着 \(to\) 指针跳即可,如果最后的位置小于等于 \(n\),说明 \(t\) 是 \(s\) 的一个子序列,反之不是。
bool check(char *t){
int m=strlen(t+1),loc=0;
for(int i=1;i<=m;++i){
loc=to[loc][t[i]-'a'];
if(loc==n+1)return 0;
}
return 1;
}
字符集较大
容易发现需要区间复制、单点修改、单点查询操作,可以使用可持久化线段树,做到 \(O(n\log|\sum|)\) 的时间复杂度。
模板
LG5826 【模板】子序列自动机
给定一个长度为 \(n\) 的正整数序列 \(a\) ,有 \(q\) 次询问,第 \(i\) 次询问给定一个长度为 \(L_i\) 的序列 \(b_i\),请你判断 \(b_i\) 是不是 \(a\) 的子序列。序列 \(a\) 和所有 \(b_i\) 中的元素都不大于一个给定的正整数 \(m\)。
\(1 \leq n, m, q \leq 10^5\),\(1 \leq a_i, b_{i, j} \leq m\),\(1 \leq l_i \leq 10^6\),\(\sum_{i = 1}^{q} l_i \leq 10^6\)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||'9'<ch){if(ch=='-')f=-1;ch=getchar();}
while('0'<=ch&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return x*f;
}
const int N=100005,M=1000006;
int n,T,m,a[N];
struct pretree{int lc,rc,val;}t[N<<5];
int root[N],tot;//可持久化线段树
void build(int &p,int l,int r){
p=++tot;
if(l==r){t[p].val=n+1;return;}
int mid=(l+r)>>1;
build(t[p].lc,l,mid);
build(t[p].rc,mid+1,r);
}
void update(int &p,int o,int l,int r,int L,int v){
p=++tot;
t[p]=t[o];
if(l==r){t[p].val=v;return;}
int mid=(l+r)>>1;
if(L<=mid)update(t[p].lc,t[o].lc,l,mid,L,v);
else update(t[p].rc,t[o].rc,mid+1,r,L,v);
}
int query(int p,int l,int r,int L){
if(l==r)return t[p].val;
int mid=(l+r)>>1;
if(L<=mid)return query(t[p].lc,l,mid,L);
else return query(t[p].rc,mid+1,r,L);
}
int main(){
read();n=read(),T=read(),m=read();
for(int i=1;i<=n;++i)a[i]=read();
build(root[n],1,m);
for(int i=n;i>=1;--i)
update(root[i-1],root[i],1,m,a[i],i);
while(T--){
int len=read(),loc=0;bool flag=1;
if(len>n)flag=0;
while(len--){
int x=read();
if(!flag)continue;//先读数再跳过
loc=query(root[loc],1,m,x);
if(loc==n+1)flag=0;
}
puts(flag?"Yes":"No");
}
return 0;
}
题目
目前找到的题目不多
LG7469 [NOI Online 2021 提高组] 积木小赛
给定两个字符串,\(s,t\) 。求 \(t\) 的本质不同子串同时是 \(s\) 的一个子序列的个数。\(len_s,len_t\leq 3\times 10^3\)。
可以考虑 \(O(n^2)\) 地枚举 \(t\) 的子串,再 \(O(n)\) 地判断是否为 \(s\) 的子序列。
发现这个过程简化。每确定一个 \(t\) 的子串的左端点,就一次扫右端点,每加入一个字符就在 \(s\) 的子序列自动机上跳指针,并判断合法。时间复杂度 \(O(n^2)\) 。
同时使用hash对 \(t\) 的子串判重。实现难度全在hash上了,代码就不贴了。
LG3856 [TJOI2008]公共子串
求三个字符串 \(s_0,s_1,s_2\) 的公共子序列有多少个。字符串长均不超过 \(100\) 。
设 \(f_{i,j,k}\) 表示从 \(s_0\) 的第 \(i\) 个字母,\(s_1\) 的第 \(j\) 个字母,\(s_2\) 的第 \(k\) 的字母开始到结束的字符串都包含的本质不同子串个数。
则可以得到转移
可以通过记忆化搜索实现,这样就可以不用访问很多不合法的状态。同时对于每搜索到的位置,其本身就可作为一个子序列,故可赋初值 \(1\) 。
代码
const int N=102;
char s[3][N];int len[3];
int to[3][N][26],las[26];
ll dp[N][N][N];
inline ll dfs(int x,int y,int z){
if(dp[x][y][z])return dp[x][y][z];
if(x||y||z)dp[x][y][z]=1ll;
for(int i=0;i<26;++i)
if(to[0][x][i]!=len[0]+1&&to[1][y][i]!=len[1]+1&&to[2][z][i]!=len[2]+1)
dp[x][y][z]+=dfs(to[0][x][i],to[1][y][i],to[2][z][i]);
return dp[x][y][z];
}
int main(){
for(int i=0;i<3;++i){
scanf("%s",s[i]+1);
len[i]=strlen(s[i]+1);
for(int j=0;j<26;++j)las[j]=len[i]+1;
for(int j=len[i];j>=0;--j){
for(int k=0;k<26;++k)to[i][j][k]=las[k];
las[s[i][j]-'a']=j;
}
}
printf("%lld\n",dfs(0,0,0));
return 0;
}
LG4608 [FJOI2016]所有公共子序列问题
求两个字符串 \(s,t\) 的公共子序列数。给出 \(opt\),若 \(opt=1\) 则输出所有公共子序列和答案,反之输出答案。其中 \(s,t\) 内为大小写字母,\(len_s,len_t \leq 3\times 10^3\)。
其实做法和LG3856是一样的,之所以单独提出来,是因为这题太毒瘤了!
对于 \(opt=1\) 的情况,考虑暴力dfs。但是要注意这题空串也算子序列。
inline void pp(){
for(int i=1;i<=top;++i)
putchar(stk[i]);
putchar('\n');
++int_ans;
}
void dfs(int x,int y){
pp();
for(int i=0;i<52;++i){
if(to1[x][i]!=n+1&&to2[y][i]!=m+1){
stk[++top]=itc(i);//数字转字母
dfs(to1[x][i],to2[y][i]);
--top;
}
}
}
对于 \(opt=2\) 的情况,还要写高精度,并且如果用结构体写高精度,不要在开数组的时候初始化,这样系统会自动帮你压掉不会用的数组,不然就会傻傻地开 \(9\times 10^6\) 个高进度数组,直接MLE。
太长了,就不贴代码了。
update in 2022.08.09
P4112 [HEOI2015]最短不公共子串
给两个小写字母串 \(a, b~(|a|,|b|\leq 2\times 10^3)~\),请你计算:
- \(a\) 的一个最短的子串,它不是 \(b\) 的子串。
- \(a\) 的一个最短的子串,它不是 \(b\) 的子序列。
- \(a\) 的一个最短的子序列,它不是 \(b\) 的子串。
- \(a\) 的一个最短的子序列,它不是 \(b\) 的子序列。
后缀自动机可以记录任意一个子串的状态,而子序列自动机可以记录任意一个子序列的状态。
对 \(a,b\) 建出各自的后缀自动机和子序列自动机。
从后缀自动机或子序列自动机的根节点(即一个字符都没有匹配的状态)开始进行广搜。广搜时记录在 \(a\) 串上的状态、\(b\) 串上的状态和目前匹配的长度,枚举转移边,如果都能匹配上就加入队列,否则如果 \(a\) 能匹配上而 \(b\) 失配,则当前匹配长度 \(+1\) 显然就是最优答案。
同时会有很多搜索到达的状态重复。因为子序列自动机和后缀自动机的状态数均是线性的,所以可以开一个二维数组记录到达过的状态。
具体实现的时候自动机中只有一个节点的儿子对我们是有用的,所以可以将两个不同自动机的 ch
数组“抠出来”。能避免写四次 BFS 。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N=4003;
struct SAM{
int fat[N],len[N],ch[N][26],tot,las;
SAM(){las=tot=1;}
inline void insert(int c){
int u=++tot,p=las;
len[u]=len[p]+1;
for(;p&&!ch[p][c];p=fat[p])
ch[p][c]=u;
if(!p)fat[u]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1)fat[u]=q;
else{
int tmp=++tot;
len[tmp]=len[p]+1;
fat[tmp]=fat[q];
memcpy(ch[tmp],ch[q],sizeof(ch[tmp]));
for(;p&&ch[p][c]==q;p=fat[p])
ch[p][c]=tmp;
fat[u]=fat[q]=tmp;
}
}
las=u;
}
inline void build(char *s){
int n=strlen(s+1);
for(int i=1;i<=n;++i)
insert(s[i]-'a');
}
}sam1,sam2;
struct LAM{
int ch[N][26],las[N],fat[N],tot;
LAM(){tot=1;fill(las,las+26,1);}
inline void insert(int c){//太上流了
int p=las[c];fat[++tot]=p;
for(int i=0;i<26;++i)
for(int j=las[i];j&&!ch[j][c];j=fat[j])
ch[j][c]=tot;
las[c]=tot;
}
inline void build(char *s){
int n=strlen(s+1);
for(int i=1;i<=n;++i)
insert(s[i]-'a');
}
}lam1,lam2;
int nxt1[N][26],nxt2[N][26],vis[N][N],tim;
struct node{
int x,y,t;
};
inline int solve(){
queue<node>q;
++tim;vis[1][1]=tim;
q.push((node){1,1,0});
while(!q.empty()){
int x=q.front().x,y=q.front().y,t=q.front().t;q.pop();
for(int i=0;i<26;++i){
int v1=nxt1[x][i],v2=nxt2[y][i];
if(v1&&v2){
if(vis[v1][v2]==tim)continue;
q.push((node){v1,v2,t+1});
vis[v1][v2]=tim;
}
if(v1&&!v2)return t+1;
}
}
return -1;
}
int main(){
char s1[N],s2[N];
scanf("%s%s",s1+1,s2+1);
sam1.build(s1),sam2.build(s2);
lam1.build(s1),lam2.build(s2);
memcpy(nxt1,sam1.ch,sizeof(nxt1));
memcpy(nxt2,sam2.ch,sizeof(nxt2));
printf("%d\n",solve());//子串--子串
memcpy(nxt2,lam2.ch,sizeof(nxt2));
printf("%d\n",solve());//子串--子序列
memcpy(nxt1,lam1.ch,sizeof(nxt1));
memcpy(nxt2,sam2.ch,sizeof(nxt2));
printf("%d\n",solve());//子序列--子串
memcpy(nxt2,lam2.ch,sizeof(nxt2));
printf("%d\n",solve());//子序列--子序列
return 0;
}// 296ms / 37.97MB / 2.03KB C++14 (GCC 9) O2
本文作者:BigSmall_En
本文链接:https://www.cnblogs.com/BigSmall-En/p/16530122.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步