子序列自动机 学习笔记

因为在某场比赛上没学过子序列自动机而被搞了心态,听说是最简单的一种自动机,就赶紧来点亮技能树(

简介

子序列自动机,好像也称序列自动机,是一种可以快速判断字符串 t 是否是字符串 s 子串的算法。

这是一个很朴素的算法,本质上就是利用空间来换取时间。

实现

1. 原理

假设现有一字符串 s ,我们定义一数组 nxti,j ,其意义为对于串 s ,从第 i 个位置后的字符串中元素 j 首次出现的位置。此时对于另一串 t ,若 ts 的子串,则 t 的每个位置的元素,可以利用 nxt 数组在 s 上的剩余字串得到匹配。

2. 构造 nxt 数组

对于构造 nxt 数组,我们可以从后往前枚举原串的每一个位置,先进行 nxti,j=nxti+1,j 的初始化,在根据原串当前位置元素对 nxt 数组进行修改。

代码如下:

for(int i=n-1;i>=0;i--)
{
    for(int j=1;j<=m;j++) nxt[i][j]=nxt[i+1][j];
    nxt[i][s[i+1]]=i+1;
}

时间复杂度为 O(|S|m) ,其中 m 为元素个数。

3. 查找

构造出 nxt 数组后我们就可以进行查找子串操作。

我们定义指针 now=1 ,对于每次匹配,指针 now 都跳转到 nxtnow+1,t[j] 的位置。如果当前 now 的指向值为空(或者为 n ,取决于 nxt 的初始化),则说明母串中无法找到子串 t

代码如下:

int p=-1;
for(int i=0;i<n;i++)
{
    p=nxt[p+1][t[i]];
    if(!p) return false;
}
return true;

时间复杂度为 O(Σ|T|)

4. 其他优化

我们来看看这道模板 P5826 【模板】子序列自动机

瞄一眼数据范围,n105 ,先不论时间够不够,空间是妥妥的会炸。

根据子序列自动机的思路,为了找第 i 位置后 j 元素的位置,我们可以考虑将原串中每一个 j 元素的位置记录下来,二分出第一个大于 i 的位置。

所以我们可以开 mvector 数组,存下下标,进行二分。

代码如下:

vector<int> nxt[MAXN];
int main()
{
	type=Read();n=Read();q=Read();m=Read();
	for(int i=1;i<=n;i++)
	{
		int x=Read();
		nxt[x].push_back(i);
	}
	for(int i=1;i<=q;i++)
	{
		int len=Read(),now=0,flag=1;
		for(int j=1;j<=len;j++)
		{
			int x=Read();
			if(!flag) continue;
			vector<int>::iterator it=lower_bound(nxt[x].begin(),nxt[x].end(),now+1);
			if(it==nxt[x].end()) flag=0;
			else now=*it;
		}
		if(flag) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

时间复杂度为 O(n+(Σ|T|)logn) ,空间复杂度为 O(n+m)

好像可以用主席树来写,但是我不会qwq

哪天会了来补

例题

  • 判断是否原串子序列

子序列自动机的基础应用。

  • 求一个字符串的子序列个数

处理出 nxt 数组之后可以对该串进行 dp

f[v][j] 表示从 1v 中长度为 j 的子序列个数

则易得 f[v][j]=nxt[u][i]=vf[u][j1]

queue<int> q;
int f[MAXN][MAXN],d[MAXN];
void Dp()
{
    f[0][0]=1;
    q.push(0);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=1;i<=m;i++)
        {
            if(!nxt[u][i]) continue;
            for(int j=0;j<=u;j++) f[nxt[u][i]][j+1]+=f[u][j];
            d[nxt[u][i]]--;
            if(!d[nxt[u][i]]) q.push(nxt[u][i]);
        }
    }
}

其实可以看出这个本质上就是在 DAG 上跑拓扑 dp ,所以 nxt 数组本质上就是一个 DAG

  • 求两串的公共子序列个数

将两串的 nxt 数组先处理出来,设 f[i][j] 表示 两串分别以位置 i 和位置 j 为起点的公共子序列个数,然后 Dfs 一下就行了。

int f[MAXN][MAXN];
int Dfs(int i,int j)
{
    if(f[i][j]) return f[i][j];
    for(int now=1;now<=n;now++)
        if(nxta[i][now]&&nxtb[j][now])
            f[i][j]+=Dfs(nxta[i][now],nxtb[j][now]);
    return ++f[i][j];
}

可以根据需要用 map 来储存个数。

  • 求字符串的回文子序列个数

回文其实可以看作将原串分成两部分,由两端查找公共子序列。

所以可以正向和反向处理出 nxt 数组,注意反向处理的时候从右到左位置递增,然后进行 Dfs ,易得当匹配的两位置 x+yn+1 时记录答案。由于这里我们只能统计长度为偶数的子序列个数,对于奇数长度的我们需要另加一。

int f[MAXN][MAXN];
int Dfs(int x,int y)
{
    if(f[x][y]) return f[x][y];
    for(int i=0;i<26;i++)
    {
        if(nxtl[x][i]+nxtr[y][i]>n+1) continue;
        if(nxtl[x][i]+nxtr[y][i]<n+1) f[x][y]++;
        f[x][y]+=Dfs(nxtl[x][i],nxtr[y][i]);
    }
    return ++f[x][y];
}

参考资料

题解 P5826 【【模板】子序列自动机】

题解 P1819 【公共子序列_NOI导刊2011提高(03)】

更新中(

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