CCF NOI Online 2021 提高组 T2 积木小赛 (子序列自动机+后缀自动机,O(n^2))

题面

Alice 和 Bob 最近热衷于玩一个游戏——积木小赛。

Alice 和 Bob 初始时各有 n 块积木从左至右排成一排,每块积木都被标上了一个英文小写字母。

Alice 可以从自己的积木中丢掉任意多块(也可以不丢);Bob 可以从自己的积木中丢掉最左边的一段连续的积木和最右边的一段连续的积木(也可以有一边不丢或者两边都不丢)。两人都不能丢掉自己所有的积木。然后 Alice 和 Bob 会分别将自己剩下的积木按原来的顺序重新排成一排。

Alice 和 Bob 都忙着去玩游戏了,于是想请你帮他们算一下,有多少种不同的情况下他们最后剩下的两排积木是相同的。

两排积木相同,当且仅当这两排积木块数相同且每一个位置上的字母都对应相同。两种情况不同,当且仅当 Alice(或者 Bob)剩下的积木在两种情况中不同。

【输入格式】

从文件 block.in 中读入数据。
第一行一个正整数 n,表示积木的块数。
第二行一个长度为 n 的小写字母串 s,表示 Alice 初始的那一排积木,其中第 i 个
字母 si 表示第 i 块积木上的字母。
第三行一个长度为 n 的小写字母串 t,表示 Bob 初始的那一排积木,其中第 i 个字
母 ti 表示第 i 块积木上的字母。

【输出格式】

输出到文件 block.out 中。
一行一个非负整数表示答案。

【数据范围与提示】

对于所有测试点:1 ≤ n ≤ 3000,s 与 t 中只包含英文小写字母。
测试点 1 满足:n ≤ 3000,s 与 t 中只包含同一种字母。
测试点 2 ∼ 4 满足:n ≤ 100。
测试点 5 ∼ 7 满足:n ≤ 500。
测试点 8 ∼ 10 满足:n ≤ 3000。

题解

这题的 n 如此小,我们可以暴力枚举 t 中每个子串。怎么判断该子串是否是 s 的子序列且之前没出现过呢?

首先,判断是否是 s 的子序列,我们可以构建一个子序列自动机,即拿一个字符串在上面跑,能跑到 s 的本质不同的每个子序列,类似跑子串的后缀自动机,只不过它更简单。原理:每个点的 sonc 存它后面第一个字符 c 所在的位置,基于贪心的思路,一定可以保证子序列都能跑到。构建子序列自动机极其简单,只需要倒着跑一遍字符串,当前点继承上一个点,并更新每个字符最近的位置就行,复杂度 O(n*字符集大小):

for(int i = len-1;i > 0;i --) A[i]=A[i+1],A[i].son[s[i+1]-'a']=i+1;

然后要判断 t 的当前子串是否之前出现过。哈希判断会多一个 log ,官方题解里并没加上。虽然可以过,但是不是很优(不然这篇文章意义何在?肯定是官方题解里没有的才讲)。于是我们可以对 t 建一个后缀自动机,然后拿 t 的子串(从左到右的顺序)在上面匹配😓,同时在 s 的子序列自动机上跑,判断有无在 s 中出现。后缀自动机和 Parent Tree 高度统一,因此,我们判断该子串合法时,为了去重,我们在后缀自动机上的对应节点(此时 SAM 上一定跑到了一个点的)存第一次出现的右端位置。因为 Parent Tree 相当于前缀树,所以要存右端点,若当前跑到这个节点时,已经存的有比它的右端点小的位置,那么说明在之前该子串出现过(根据 endpos 的定义),就不计数。若等于它的右端点,说明它出现在之前某个合法串的后缀中,还是要计数的。

于是,我们就得到了一个 O(26 n + n2) = O(n2) 的算法,比官方题解优(用哈希+基排的当我没说)。

CODE

考场代码,未经修饰:

#include<set>
#include<map>
#include<queue>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 3005
#define MAXC 26
#define DB double
#define LL long long
#define ENDL putchar('\n')
#define lowbit(x) (-(x) & (x))
LL read() {
	LL f = 1,x = 0;char s = getchar();
	while(s < '0' || s > '9') {if(s=='-')f = -f;s = getchar();}
	while(s >= '0' && s <= '9') {x=x*10+(s-'0');s = getchar();}
	return f * x;
}
int n,m,i,j,s,o,k;
struct SAM{
	int s[MAXC+2];
	int fa,len;
	SAM(){fa=len=0;memset(s,0,sizeof(s));}
}sam[MAXN<<1],sub[MAXN];
int las = 1,cnt = 1;
int pos[MAXN<<1];
void addsam(int c) {
	int p = las;int np = (las = ++ cnt);
	sam[np].len = sam[p].len + 1;
	for(;p && !sam[p].s[c];p = sam[p].fa) sam[p].s[c] = np;
	if(!p) sam[np].fa = 1;
	else {
		int q = sam[p].s[c];
		if(sam[q].len == sam[p].len + 1) sam[np].fa = q;
		else {
			int nq = ++ cnt; sam[nq] = sam[q];
			sam[nq].len = sam[p].len + 1;
			sam[np].fa = sam[q].fa = nq;
			for(;p && sam[p].s[c] == q;p = sam[p].fa) sam[p].s[c] = nq;
		}
	}
	return ;
}
char s1[MAXN],s2[MAXN];
int main() {
	freopen("block.in","r",stdin);
	freopen("block.out","w",stdout);
	n = read();
	scanf("%s",s1 + 1);
	scanf("%s",s2 + 1);
	for(int i = 1;i <= n;i ++) {
		addsam(s2[i]-'a');
	}
	for(int i = n-1;i >= 0;i --) {
		sub[i] = sub[i+1];
		sub[i].s[s1[i+1]-'a'] = i+1;
	}
	int ans = 0;
	for(int i = 1;i <= n;i ++) {
		int p1 = 0,p2 = 1;
		for(int j = i;j <= n;j ++) {
			int cc = s2[j]-'a';
			if(!sub[p1].s[cc]) break;
			if(!sam[p2].s[cc]) break;
			p1 = sub[p1].s[cc];
			p2 = sam[p2].s[cc];
			if(pos[p2] == 0) pos[p2] = j;
			if(pos[p2] >= j) ans ++;
		}
	}
	printf("%d\n",ans);
	return 0;
}
/*
20
egebejbhcfabgegjgiig
edfbhhighajibcgfecef
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值