【字符串】序列自动机

  • ~1.56K 字
  1. 1. 状态
  2. 2. 性质
  3. 3. 构建
    1. 3.1. 字符集较小
    2. 3.2. 字符集较大
  4. 4. 应用

序列自动机是接受且仅接受一个字符串的子序列的自动机

状态

由子序列的性质,若s包含n个字符,则构造的序列自动机包含个状态,且每个状态都是终止状态

因此我们可以通过序列自动机获得它的每一个子序列

如abcab的序列自动机如下

img

我们可以维护一个指针,代表s的第i个位置后,第一个字符c所处的位置,若i后没有c了,我们默认指向

对于例子中的字符串,

这样我们可以通过一个位置x开始不断跳to指针来得到一个s的子序列

为了方便,我们可以设是s中第一个c出现的位置,这样跳指针时,设起点即可

性质

由图可以看出,前n个状态接受长度为的前缀的所有子序列,则第i个状态接受的是长度为的前缀有,长度为的前缀没有的子序列

因此我们可以动态的构造序列自动机,即每在尾部新增一个字符c,就新增一个状态p,让前面所有不能接受c的状态q接受c转移到p

构建

字符集较小

对于字符集较小,如a-z,我们可以直接维护

从后往前倒叙枚举每一个位置i,构建,维护一个数组las保存c目前出现的最靠前的位置

首先让字符集中搜友字符c都执行,接着更新

时间复杂度

1
2
3
4
5
6
7
8
9
void build()
{
for(int i=1; i<=m; i++) las[i] = n+1;
for(int i=n; i>=1; i--)
{
for(int j=1; j<=m; j++) to[i][j] = las[j];
las[s[i]] = i;
}
}

字符集较大

此时直接维护to指针显然捉襟见肘,我们可以改为使用可持久化线段树维护,即i代表的线段树为它的的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void plant0(int &o,int l,int r){
dcnt++,o=dcnt;
if(l==r){tre[o].val=n+1;return;}
int mid=(l+r)>>1;
plant0(tre[o].lv,l,mid),plant0(tre[o].rv,mid+1,r);
}

void plant(int ori,int &o,int l,int r,int wei,int val){
dcnt++,o=dcnt;
tre[o]=tre[ori];
if(l==r){tre[o].val=val;return;}
int mid=(l+r)>>1;
if(wei<=mid) plant(tre[ori].lv,tre[o].lv,l,mid,wei,val);
if(wei>mid) plant(tre[ori].rv,tre[o].rv,mid+1,r,wei,val);
}

int query(int o,int l,int r,int wei){
if(l==r)return tre[o].val;
int mid=(l+r)>>1;
if(wei<=mid) return query(tre[o].lv,l,mid,wei);
if(wei>mid) return query(tre[o].rv,mid+1,r,wei);
}

void build(){
plant0(rot[n],1,m);
for(int i=n; i>=1; i--) plant(rot[i],rot[i-1],1,m,S[i],i);
}

应用

  • 判断s是否为t的子序列

    将s扔进t的序列自动机,看是否能转移到底

  • 统计s的本质不同子序列个数

    记忆化搜索:,加一是因为空序列也是子序列

赞助喵
非常感谢您的喜欢!
赞助喵