2022暑期牛客第五场

"蔚来杯"2022牛客暑期多校训练营5

G-KFC Crazy Thursday

原题链接

题目大意

​ 题目给出一个长度数字 n n n 和一个长度为 n n n 的小写字母字符串,要求你分别输出以’k’、‘f’、'c’为结尾的回文串的个数。

​ 比如说,对于回文串 kfccfk ,以’k’、‘f’、'c’为结尾的回文串的个数均为3,则输出 3 3 3

思路

​ 一个一个地检查回文串实在是太慢了——指复杂度 O ( n 2 ) O(n^2) O(n2) ,但聪明的你一定敏锐地观察到了:一个较长的回文串必然包含许多对称中心相同但长度更小的回文串,具体来说就是 kfccfk fccf cc 这样的一家子都以 cc 中间的空隙为轴对称,我们称它们为一组回文串。每个回文串都有一个对称中心,反过来说,所有对称中心的回文串就是该字符串包含的所有回文串。

​ 下面以对称中心为核心分析。一个对称中心肯定有一个最长回文串(大不了长度为 0 0 0 ),对于这个对称中心的这个串,我这里有两句废话:没有比它更长的回文串了比它更短的都是回文串。这两个显而易见的性质保证了可以用 1 1 1 个对称中心和 1 1 1最长回文长度来表示出一组回文串的情况。

​ 这里开始简便化处理:

  1. 对称中心可能落在空隙,所以将字符串空隙填入同一字符,这个处理使得空隙和原字符等效了(空隙本身也有空格的意思,所以这个处理并没有添加额外的字符)。处理示例:#k#f#c#c#f#k# 。机智如你也许早已发现了处理回文时的越界问题,所以在此基础上再在两段添加两个不同的字符以确定边界,这样就不用手动写判断了。最终处理示例:$#k#f#c#c#f#k#^

  2. 最长回文半径代替最长回文长度。和画圆同理,在中心确定的情况下,半径比长度好用。

​ 回到题目,对于一组回文串,其后缀不重叠地分布在中心到(由半径确定的)右端点这一闭区间上,这一区间上某字符的数量即是这组回文串中以该字符为后缀的回文串的数量。统计所有组的数据即可得到答案。

求最长回文半径/长度,即是马拉车算法的作用。

​ 先试着暴力求一下:顺序枚举每个对称中心,从小到大遍历找出其最长回文半径,时间复杂度 O ( n 2 ) O(n^2) O(n2),不行。

​ 在枚举对称中心的途中,观察发现当前对称中心(A)往往包含在之前对称中心(B)的回文区间里,这也就意味着该中心(A)有时在前面有一个“长相类似”的对称点(C)。C的最长回文半径是已知的,A尝试使用该半径,若其回文区间不超过B,则B可以保证A和C完全对称,不需要额外处理。超出B的部分是不可信的,需要枚举判断,并在这个过程中将对称中心B的选点设为A以尽可能向后覆盖字符串区间。这就是马拉车算法,时间复杂度为线性。

​ 综上,题目得解。

代码

#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn=1e6+10;
char* malac(const char s[],int p[])
{
	static char str[maxn];
	int slen=strlen(s);
	for(int i=0;i<slen;i++)
	{
		str[(i<<1)|1]='#';
		str[(i+1)<<1]=s[i];
	}
	str[0]='^';
	str[(slen<<1)+1]='#';
	str[(slen<<1)+2]='$';
	int c=1,mx=1,str_ed=(slen<<1)+1;
	for(int i=1,j;i<=str_ed;i++)
	{
		j=(c<<1)-i;
		if(i+p[j]>=mx){
			p[i]=mx-i>0?mx-i:0;
			while(str[i+p[i]+1]==str[i-p[i]-1]) p[i]++;
			mx=i+p[i];
			c=i;
		}
		else{
			p[i]=p[j];
		}
	}
	return str;
}
ll k[maxn],f[maxn],c[maxn];
int n,a[maxn]; char cc[maxn>>1];
int main()
{
	scanf("%d%s",&n,cc);
	char* res=malac(cc,a);
	int reslen=(strlen(cc)<<1)+3;
	for(int i=1;i<reslen;i++)
	{
		k[i]=k[i-1]+(res[i]=='k');
	}
	for(int i=1;i<reslen;i++)
	{
		f[i]=f[i-1]+(res[i]=='f');
	}
	for(int i=1;i<reslen;i++)
	{
		c[i]=c[i-1]+(res[i]=='c');
	}
	ll ans=0;
	for(int i=1;i<reslen-1;i++)
	{
		ans+=k[i+a[i]]-k[i-1];
	}
	printf("%lld",ans);
	ans=0;
	for(int i=1;i<reslen-1;i++)
	{
		ans+=f[i+a[i]]-f[i-1];
	}
	printf(" %lld",ans);
	ans=0;
	for(int i=1;i<reslen-1;i++)
	{
		ans+=c[i+a[i]]-c[i-1];
	}
	printf(" %lld",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值