Gushing over Programming Girls|

BigSmall_En

园龄:3年2个月粉丝:3关注:5

2022-07-28 20:32阅读: 65评论: 0推荐: 0

子序列自动机

前言

不是很难理解,简单地写一下,并不严谨。

内容来自 博客

构建

字符串 \(S\) ,长度为 \(n\),从 \(1\) 开始编号。

维护指针 \(to_{i,c}(i\in[0,n])\),表示 \(S\) 的第 \(i\) 个位置第一个字符 \(c\) 所处的位置,如果 \(i\) 位置后没有 \(c\),了,默认指向 \(n+1\)

其中 \(to_0,c\) 可表示 \(S\) 中第一个出现 \(c\) 的位置。

一般情况

根据指针 \(to\) 的定义容易得到构建方式:

for(int i=0;i<26;++i)las[i]=n+1;
for(int i=n;i>=0;--i){
    for(int j=0;j<26;++j)to[i][j]=las[j];
    if(i)las[s[i]-'a']=i;
}

其中 \(las_c\) 表示目前所找到的字符 \(c\) 的下标最小的位置。

时间复杂度 \(O(n|\sum|)\)\(|\sum|\) 为字符集大小。

查询

假设现在需要查询 \(t\) 是否为 \(s\) 的一个子序列,只需要从 \(0\) 号点开始,沿着 \(to\) 指针跳即可,如果最后的位置小于等于 \(n\),说明 \(t\)\(s\) 的一个子序列,反之不是。

bool check(char *t){
    int m=strlen(t+1),loc=0;
    for(int i=1;i<=m;++i){
        loc=to[loc][t[i]-'a'];
        if(loc==n+1)return 0;
    }
    return 1;
}

字符集较大

容易发现需要区间复制、单点修改、单点查询操作,可以使用可持久化线段树,做到 \(O(n\log|\sum|)\) 的时间复杂度。

模板

LG5826 【模板】子序列自动机

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

\(1 \leq n, m, q \leq 10^5\)\(1 \leq a_i, b_{i, j} \leq m\)\(1 \leq l_i \leq 10^6\)\(\sum_{i = 1}^{q} l_i \leq 10^6\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;

inline int read(){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||'9'<ch){if(ch=='-')f=-1;ch=getchar();}
	while('0'<=ch&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=100005,M=1000006;
int n,T,m,a[N];
struct pretree{int lc,rc,val;}t[N<<5];
int root[N],tot;//可持久化线段树
void build(int &p,int l,int r){
	p=++tot;
	if(l==r){t[p].val=n+1;return;}
	int mid=(l+r)>>1;
	build(t[p].lc,l,mid);
	build(t[p].rc,mid+1,r);
}
void update(int &p,int o,int l,int r,int L,int v){
	p=++tot;
	t[p]=t[o];
	if(l==r){t[p].val=v;return;}
	int mid=(l+r)>>1;
	if(L<=mid)update(t[p].lc,t[o].lc,l,mid,L,v);
	else update(t[p].rc,t[o].rc,mid+1,r,L,v);
}
int query(int p,int l,int r,int L){
	if(l==r)return t[p].val;
	int mid=(l+r)>>1;
	if(L<=mid)return query(t[p].lc,l,mid,L);
	else return query(t[p].rc,mid+1,r,L);
}

int main(){
	read();n=read(),T=read(),m=read();
	for(int i=1;i<=n;++i)a[i]=read();
	build(root[n],1,m);
	for(int i=n;i>=1;--i)
		update(root[i-1],root[i],1,m,a[i],i);

	while(T--){
		int len=read(),loc=0;bool flag=1;
		if(len>n)flag=0;
		while(len--){
			int x=read();
			if(!flag)continue;//先读数再跳过
			loc=query(root[loc],1,m,x);
			if(loc==n+1)flag=0;
		}
		puts(flag?"Yes":"No");
	}
	return 0;
}

题目

目前找到的题目不多

LG7469 [NOI Online 2021 提高组] 积木小赛

给定两个字符串,\(s,t\) 。求 \(t\) 的本质不同子串同时是 \(s\) 的一个子序列的个数。\(len_s,len_t\leq 3\times 10^3\)

可以考虑 \(O(n^2)\) 地枚举 \(t\) 的子串,再 \(O(n)\) 地判断是否为 \(s\) 的子序列。

发现这个过程简化。每确定一个 \(t\) 的子串的左端点,就一次扫右端点,每加入一个字符就在 \(s\) 的子序列自动机上跳指针,并判断合法。时间复杂度 \(O(n^2)\)

同时使用hash对 \(t\) 的子串判重。实现难度全在hash上了,代码就不贴了。

LG3856 [TJOI2008]公共子串

求三个字符串 \(s_0,s_1,s_2\) 的公共子序列有多少个。字符串长均不超过 \(100\)

\(f_{i,j,k}\) 表示从 \(s_0\) 的第 \(i\) 个字母,\(s_1\) 的第 \(j\) 个字母,\(s_2\) 的第 \(k\) 的字母开始到结束的字符串都包含的本质不同子串个数。

则可以得到转移

\[f_{i,j,k}\gets f_{to_{0,i,c},to_{1,j,c},to_{2,k,c}} \]

可以通过记忆化搜索实现,这样就可以不用访问很多不合法的状态。同时对于每搜索到的位置,其本身就可作为一个子序列,故可赋初值 \(1\)

代码

const int N=102;
char s[3][N];int len[3];
int to[3][N][26],las[26];
ll dp[N][N][N];
inline ll dfs(int x,int y,int z){
	if(dp[x][y][z])return dp[x][y][z];
	if(x||y||z)dp[x][y][z]=1ll;
	for(int i=0;i<26;++i)
		if(to[0][x][i]!=len[0]+1&&to[1][y][i]!=len[1]+1&&to[2][z][i]!=len[2]+1)
			dp[x][y][z]+=dfs(to[0][x][i],to[1][y][i],to[2][z][i]);
	return dp[x][y][z];
}
int main(){
	for(int i=0;i<3;++i){
		scanf("%s",s[i]+1);
		len[i]=strlen(s[i]+1);
		for(int j=0;j<26;++j)las[j]=len[i]+1;
		for(int j=len[i];j>=0;--j){
			for(int k=0;k<26;++k)to[i][j][k]=las[k];
			las[s[i][j]-'a']=j;
		}
	}
	printf("%lld\n",dfs(0,0,0));
	return 0;
}

双倍经验

LG4608 [FJOI2016]所有公共子序列问题

求两个字符串 \(s,t\) 的公共子序列数。给出 \(opt\),若 \(opt=1\) 则输出所有公共子序列和答案,反之输出答案。其中 \(s,t\) 内为大小写字母,\(len_s,len_t \leq 3\times 10^3\)

其实做法和LG3856是一样的,之所以单独提出来,是因为这题太毒瘤了!

对于 \(opt=1\) 的情况,考虑暴力dfs。但是要注意这题空串也算子序列。

inline void pp(){
	for(int i=1;i<=top;++i)
		putchar(stk[i]);
	putchar('\n');
	++int_ans;
}
void dfs(int x,int y){
	pp();
	for(int i=0;i<52;++i){
		if(to1[x][i]!=n+1&&to2[y][i]!=m+1){
			stk[++top]=itc(i);//数字转字母
			dfs(to1[x][i],to2[y][i]);
			--top;
		}
	}
}

对于 \(opt=2\) 的情况,还要写高精度,并且如果用结构体写高精度,不要在开数组的时候初始化,这样系统会自动帮你压掉不会用的数组,不然就会傻傻地开 \(9\times 10^6\) 个高进度数组,直接MLE。

太长了,就不贴代码了。

update in 2022.08.09

P4112 [HEOI2015]最短不公共子串

给两个小写字母串 \(a, b~(|a|,|b|\leq 2\times 10^3)~\),请你计算:

  1. \(a\) 的一个最短的子串,它不是 \(b\) 的子串。
  2. \(a\) 的一个最短的子串,它不是 \(b\) 的子序列。
  3. \(a\) 的一个最短的子序列,它不是 \(b\) 的子串。
  4. \(a\) 的一个最短的子序列,它不是 \(b\) 的子序列。

后缀自动机可以记录任意一个子串的状态,而子序列自动机可以记录任意一个子序列的状态。

\(a,b\) 建出各自的后缀自动机和子序列自动机。

从后缀自动机或子序列自动机的根节点(即一个字符都没有匹配的状态)开始进行广搜。广搜时记录在 \(a\) 串上的状态、\(b\) 串上的状态和目前匹配的长度,枚举转移边,如果都能匹配上就加入队列,否则如果 \(a\) 能匹配上而 \(b\) 失配,则当前匹配长度 \(+1\) 显然就是最优答案。

同时会有很多搜索到达的状态重复。因为子序列自动机和后缀自动机的状态数均是线性的,所以可以开一个二维数组记录到达过的状态。

具体实现的时候自动机中只有一个节点的儿子对我们是有用的,所以可以将两个不同自动机的 ch 数组“抠出来”。能避免写四次 BFS 。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>

using namespace std;
const int N=4003;
struct SAM{
	int fat[N],len[N],ch[N][26],tot,las;
	SAM(){las=tot=1;}
	inline void insert(int c){
		int u=++tot,p=las;
		len[u]=len[p]+1;
		for(;p&&!ch[p][c];p=fat[p])
			ch[p][c]=u;
		if(!p)fat[u]=1;
		else{
			int q=ch[p][c];
			if(len[q]==len[p]+1)fat[u]=q;
			else{
				int tmp=++tot;
				len[tmp]=len[p]+1;
				fat[tmp]=fat[q];
				memcpy(ch[tmp],ch[q],sizeof(ch[tmp]));
				for(;p&&ch[p][c]==q;p=fat[p])
					ch[p][c]=tmp;
				fat[u]=fat[q]=tmp;
			}
		}
		las=u;
	}
	inline void build(char *s){
		int n=strlen(s+1);
		for(int i=1;i<=n;++i)
			insert(s[i]-'a');
	}
}sam1,sam2;
struct LAM{
	int ch[N][26],las[N],fat[N],tot;
	LAM(){tot=1;fill(las,las+26,1);}
	inline void insert(int c){//太上流了
		int p=las[c];fat[++tot]=p;
		for(int i=0;i<26;++i)
			for(int j=las[i];j&&!ch[j][c];j=fat[j])
				ch[j][c]=tot;
		las[c]=tot;
	}
	inline void build(char *s){
		int n=strlen(s+1);
		for(int i=1;i<=n;++i)
			insert(s[i]-'a');
	}
}lam1,lam2;

int nxt1[N][26],nxt2[N][26],vis[N][N],tim;
struct node{
	int x,y,t;
};
inline int solve(){
	queue<node>q;
	++tim;vis[1][1]=tim;
	q.push((node){1,1,0});
	while(!q.empty()){
		int x=q.front().x,y=q.front().y,t=q.front().t;q.pop();
		for(int i=0;i<26;++i){
			int v1=nxt1[x][i],v2=nxt2[y][i];
			if(v1&&v2){
				if(vis[v1][v2]==tim)continue;
				q.push((node){v1,v2,t+1});
				vis[v1][v2]=tim;
			}
			if(v1&&!v2)return t+1;
		}
	}
	return -1;
}

int main(){
	char s1[N],s2[N];
	scanf("%s%s",s1+1,s2+1);
	sam1.build(s1),sam2.build(s2);
	lam1.build(s1),lam2.build(s2);
	memcpy(nxt1,sam1.ch,sizeof(nxt1));
	memcpy(nxt2,sam2.ch,sizeof(nxt2));
	printf("%d\n",solve());//子串--子串
	
	memcpy(nxt2,lam2.ch,sizeof(nxt2));
	printf("%d\n",solve());//子串--子序列

	memcpy(nxt1,lam1.ch,sizeof(nxt1));
	memcpy(nxt2,sam2.ch,sizeof(nxt2));
	printf("%d\n",solve());//子序列--子串

	memcpy(nxt2,lam2.ch,sizeof(nxt2));
	printf("%d\n",solve());//子序列--子序列
	return 0;
}//	 296ms /  37.97MB /  2.03KB C++14 (GCC 9) O2

本文作者:BigSmall_En

本文链接:https://www.cnblogs.com/BigSmall-En/p/16530122.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   BigSmall_En  阅读(65)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起