子序列自动机 学习笔记

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

简介

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

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

实现

1. 原理

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

2. 构造 \(nxt\) 数组

对于构造 \(nxt\) 数组,我们可以从后往前枚举原串的每一个位置,先进行 \(nxt_{i,j}=nxt_{i+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\) 都跳转到 \(nxt_{now+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(\Sigma|T|)\)

4. 其他优化

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

瞄一眼数据范围,\(n ≤ 10^5\) ,先不论时间够不够,空间是妥妥的会炸。

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

所以我们可以开 \(m\)\(vector\) 数组,存下下标,进行二分。

代码如下:

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+(\Sigma|T|)logn)\) ,空间复杂度为 \(O(n+m)\)

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

哪天会了来补

例题

  • 判断是否原串子序列

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

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

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

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

则易得 \(f[v][j]=\sum\limits_{nxt[u][i]=v}f[u][j-1]\)

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+y≤n+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 @ 2024-09-26 00:05  zhln  阅读(210)  评论(0)    收藏  举报