序列自动机

引入

给定一个长度为 n 的正整数序列 a ,有 q 次询问,第 i 次询问给定一个长度为 Li 的序列 bi,请你判断 bi 是不是 a 的子序列。序列 a 和所有 bi 中的元素都不大于一个给定的正整数 m

1n,m,q1051ai,bi,jm1li106i=1qli106

做法

考虑维护一个能够维护某个串的所有本质不同的子序列的自动机。


序列自动机

可能是最简单构造/理解的,附加内容最少的自动机。

构造

考虑贪心。对于一个串 s,从某个位置开始维护子序列时,显然让每个字符均在 s 尽可能靠前的位置出现时,后面能够接的子序列数会尽量多。这样不会减少能够构造的数量的子序列数量。

son(i,c) 表示 si 之后第一个字符 c 出现的位置。将 son(i,c) 抽象成转移边,则所有的 son 转移边构成了自动机。而 son 的求法也很简单,逆推即可:

son(i,c)={son(i+1,c)si+1ci+1si+1=c

字符集较大时,可以用主席树维护转移边。

模板代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=18;
const int maxn=1000010;
int n,m,q,i,lt,rt,mt,pt,qt,tot;
int a[maxn];
struct node{
int ls,rs,val;
}tr[maxn*maxl];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define val(p) tr[p].val
int root[maxn];
int main(){
scanf("%d",&n);
scanf("%d%d%d",&n,&q,&m);
for(i=1;i<=n;++i) scanf("%d",a+i);
root[n]=tot=1;
for(i=n;i;--i){
pt=root[i-1]=++tot;
lt=1;rt=m;qt=root[i];
while(lt<rt){
tr[pt]=tr[qt];
mt=(lt+rt)>>1;
if(a[i]<=mt){
ls(pt)=++tot;
rt=mt;qt=ls(qt);
}
else{
rs(pt)=++tot;
lt=mt+1;qt=rs(qt);
}
pt=tot;
}
val(pt)=i;
}
while(q--){
scanf("%d",&n);
for(i=1;i<=n;++i) scanf("%d",a+i);
pt=root[0];
for(i=1;i<=n;++i){
lt=1;rt=m;
while(lt<rt){
mt=(lt+rt)>>1;
if(a[i]<=mt){
pt=ls(pt);
rt=mt;
}
else{
pt=rs(pt);
lt=mt+1;
}
}
pt=val(pt);
if(!pt){
printf("No\n");
break;
}
else pt=root[pt];
}
if(pt) printf("Yes\n");
}
return 0;
}

例题:

给出字符串 S,计算其本质不同的子序列数,答案取模。|S|3×105

附加内容 1:无。

直接求自动机上不同路径数即可。

附加内容 2:求其 k 小子序列。k1018

附加内容 2.5k109,|S|106

在子序列自动机的 DAG 上找出从某个点出发的路径条数(根节点之外的点的路径条数要加一),大于 k 的直接赋为 inf(打 inf 标记),然后模拟 dfs 过程,按位填字符即可,方法可以参照 P3975 [TJOI2015]弦论 的做法。注意原题空间只有 125MB,故在 DAG 上 dp 找路径条数时,不应该建反图然后拓扑 dp,而是 dfs 以递归查找。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
const int maxx=1000000000;
const int Inf=1000000001;
int n,k,i,j,l,r,u,v,tot;
int num[maxn],nxt[maxn][26];
char s[maxn];
bool vis[maxn];
void dfs(int p){
if(vis[p]) return;
vis[p]=1;
int lp,to;
for(lp=0;lp<26;++lp){
to=nxt[p][lp];
if(!to) continue;
dfs(to);
num[p]+=num[to];
if(num[p]>maxx){
num[p]=Inf;
return;
}
}
}
int main(){
scanf("%d%d%s",&n,&k,s+1);
for(i=n;i;--i){
num[i]=1;
for(j=25;j>=0;--j) nxt[i-1][j]=nxt[i][j];
nxt[i-1][s[i]-'a']=i;
}
dfs(0);
++k;
for(;;){
if(k==1) return 0;
--k;
for(j=0;j<26;++j){
v=nxt[u][j];
if(!v) continue;
if(k<=num[v]){
u=v;
putchar(j+'a');
break;
}
else k-=num[v];
}
}
return 0;
}

附加内容3:q 组询问,每次询问第 k 小子序列的最后 r 个字符。k1018,q105,r106

待补。

posted @   Fran-Cen  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示