[字符串]------[序列自动机]

序列自动机

一般来说算法竞赛中出现的有关于字符串的 “自动机算法” 都比较高大上,然而 “序列自动机” ----- 一开始我根本没懂这个东西怎么沾上的 “自动机” 的边,但是结合前几天学习 “AC自动机” 有一篇博客中讲的一句话:AC自动机是 “被极端简化了的确定有限状态自动机”,忽然意识到,这个 “序列自动机” 不像自动机的原因 ------ 它被简化到只剩一个数组了。

序列自动机可以快速判断若干字符串 s1,s2,…sk 中的每一个,是否是字符串 T 的子序列,还有解决这个问题的一些衍生问题。

序列自动机的构造

假设现在我要判断字符串 S = s1s2s3…sn 是否是 T 的子序列(lenS<=lenT),我首先要在 T 中寻找 s1 这个字符,那么是不是我必须找出 T 中所有的 s1 字符呢?很显然不需要,我只需要贪心地在 T 里取最前面的一个 s1 即可,因为如果用后面的 s1 可以构造出字符串 S,那么用前面的也可以,反之则不一定。所以我们每次贪心地取 S 中的每个字符在 T 中最前面的一个。
我们用 N e x t [ i ] [ j ] Next[i][j] Next[i][j] 表示位置 i 后面最近的字符 j 的下标(这里要给字符集中的每个元素编个号,例如如果字符集是’a’~‘z’,可以编号 1~26)。如果位置 i 后面找不到字符 j , N e x t [ i ] [ j ] = 0 Next[i][j] = 0 Next[i][j]=0
构造母串的Next数组:

void Init()
{
	int len = strlen(str);
	for(int i=len;i>=1;i--)        //显然需要从后向前构造
	{
		for(int j=1;j<=26;j++)     //字符集是26个小写字母
		    Next[i-1][j] = Next[i][j];
		Next[i-1][str[i-1]-'a'+1] = i;
	}
}

Next 数组的第二维要开字符集大小,并且字符集中的每一个字符要有一个整数映射。例如如果模板串和模式串都是小写字母组成的,定义 N e x t [ l e n ] [ 27 ] Next[len][27] Next[len][27]。虽然这样定义有一个很大的缺陷,但这是最常用的一种序列自动机,已经可以解决大多数(简单)问题了。后面会写怎样优化它。

在验证一个字符串是否是模板串的子序列的时候,只需对每一个字符顺着 Next 数组向后找,如果存在找不到的字符,就不是模板串的子序列。

for(int i=1;i<=N;i++)        //假设询问N个字符串是否是模板串的子序列
{
	scanf("%s", str2);
	int len = strlen(str2);
	int ok = 1, now = 0;
	for(int i=0;i<len;i++)
	{
		now = Next[now][str2[i]-'a'+1];     //找下一个字符的位置 
		if(!now)           //找不到就不是子序列 
		{
			ok = 0;
			break;
		}
	}
	if(ok)
	    printf("Yes\n");
	else
	    printf("No\n");
}

当询问的字符串很多时,序列自动机比暴力寻找子序列要快很多。因为跳过了很多不必要字符。

序列自动机的经典问题

  1. 求两个字符串不同的公共子序列个数。

假装不知道有DP。。。
对两个字符串分别构造 Next 数组 用记忆化搜索(可能过渡一下就得到了DP??,我DP没学好。。)

ll DFS(int x, int y)    //表示目前字符是串1的第x位,串2的第y位
{
	if(data[x][y])
	    return data[x][y];
	for(int i=1;i<=26;i++)
	    if(next1[x][i]&&next2[y][i])
	        data[x][y] += DFS(next1[x][i], next2[y][i]);
	if(x||y)
	    ++data[x][y];
	return data[x][y];
}
  1. 求一个字符串的回文子序列个数

分别对原串和反串构造 Next 数组,可以想到原串的回文子序列在这里体现为原串和反串的公共子序列,因为回文串正反是一样的。这样就把问题划归为了第一个问题,但是我们要注意两个地方:

  1. 因为构造出来的原串和反串本质上是一个串,相当于从左右端点向中间找子序列,因此DFS时参数也有限制,当 x + y ≤ l e n + 1 x+y≤len+1 x+ylen+1 才是合法的
  2. 回文串分奇串和偶串,直接DFS会丢掉一部分奇串,这部分需要手动加上。
int dfs(int x, int y)
{
	if(data[x][y]) 
	    return data[x][y];
	for(int i=1;i<=26;i++)
		if(next1[x][i]&&next2[y][i])
		{
			if(next1[x][i]+next2[y][i]>len+1) 
			    continue;
			if(next1[x][i]+next2[y][i]<n+1) 
			    data[x][y]++;
			data[x][y] += dfs(next1[x][i], next2[y][i]));
		}
	return ++data[x][y];
}
  1. 给定三个串A,B,C,求一个最长的A,B的公共子序列S,要求C是S的子序列。

还是同样的dfs(x, y, z),表示一匹配到 C 的第 z 位,需要改变一下 C 的Next数组构造方式。

for(int i=1;i<=26;++i) 
    next[lenc][i] = lenc;
for(int i=0;i<lenc;++i)
{
	for(int j=1;j<=26;++j) 
	    next[i][j] = i;
	next[i][c[i+1]] = i+1;
}

真正的序列自动机

前面写到了:构造 Next 数组,有一个很大的缺陷。这个缺陷就是:这种思路只适用于字符集较小的情况,如果字符集很大(例如:字符集是 1 到 1e5 之间的所有数字),这样还想像刚才一样构造 N e x t [ l e n ] [ 1 e 5 ] Next[len][1e5] Next[len][1e5] 数组,时间和空间都不允许。
我们再看一下构造 Next 数组的代码:

void Init()
{
	int len = strlen(str);
	for(int i=len;i>=1;i--)        //显然需要从后向前构造
	{
		for(int j=1;j<=26;j++)     //字符集是26个小写字母
		    Next[i-1][j] = Next[i][j];
		Next[i-1][str[i-1]-'a'+1] = i;
	}
}

注意到: N e x t [ i − 1 ] [ j ] Next[i-1][j] Next[i1][j] 实际上大部分直接继承了 N e x t [ i ] [ j ] Next[i][j] Next[i][j]的值,因为只有 S[i] 这一项会影响到当前的 Next[j],所以 N e x t [ i − 1 ] [ j ] Next[i-1][j] Next[i1][j] N e x t [ i ] [ j ] Next[i][j] Next[i][j]只有一位不同,因此这个数组的第一维没有什么实际必要,可以想办法去掉。我们想到,i 每向前移动一位,就改变一个数组值,然后还需要随时访问改变以前的数组值(因为从前向后验证子序列),可以看出来,这是一个可持久化数组,这个操作是主席树的单点修改
我们对 N e x t Next Next 数组建主席树,主席树的每一个 Root,就对应了一个 Next 数组。还是从后向前构造,i 每向前移动一位,就把数组第 str[i] 个元素改成 i,这样对主席树 Root[i] 询问当前这这棵线段树的某个元素,就是离当前 i 结点最近的这个字符。

for(int i=1;i<=N;i++)         //这是原序列(母串)
	scanf("%d", &A[i]);
for(int i=N;i>=1;i--)         //主席树的单点修改
{
	root[i-1] = update(root[i], 1, N, A[i], i);
	//root[i-1] 是 root[i] 将A[i]这一项修改为i
}
//用 query(root[pos], 1, N, t[j]); 主席树的单点查询,来查询pos后最近的 t[j] 字符

例题

普通序列自动机模板:
牛客小白月赛12:月月查华华的手机.

三个串的公共子序列计数(和两个串的本质相同):
洛谷P1819 公共子序列.

真·序列自动机:
洛谷P5826 【模板】子序列自动机.

毒瘤题目,序列自动机+动态规划+高精度:
洛谷P4608 [FJOI2016]所有公共子序列问题.
这个题目序列自动机的部分其实很好理解,然而在后面两个东西的加持下,它变得很困难。(这道题笔者本人拿不到满分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值