子序列自动机 学习笔记
因为在某场比赛上没学过子序列自动机而被搞了心态,听说是最简单的一种自动机,就赶紧来点亮技能树(
简介
子序列自动机,好像也称序列自动机,是一种可以快速判断字符串 \(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];
}
参考资料
题解 P1819 【公共子序列_NOI导刊2011提高(03)】
更新中(

浙公网安备 33010602011771号