【马拉车】【kmp】【前缀和】LGP6216 回文匹配

题意

给定文本串 T ,模式串 S ,求 ST 的所有奇数回文子串中出现的次数。答案取 32 位无符号整数自然溢出的结果。时空复杂度要求线性。

思路

奇数回文子串,马拉车狂喜,都不用进行字符串处理。

出现次数,也就是字符串匹配,记录下出现的位置,考虑 kmp,得到某个位置是否为模式串末尾的一个 01 数组。

然后根据马拉车得到的结果,可以枚举所有的奇数回文子串,然后使用前缀和处理一下 kmp 的结果可以求出奇数回文子串中模式串出现的个数。

大概就是这样子

for(int i=1;i<=n;++i) for(int j=0;j<mnchr[i];++j) ans+=kmp[i+j]-kmp[i-j-1];

这样可以得到一份《红与黑》的代码。

黑很容易理解,因为这样的时间复杂度实在是太高了,一不小心就会退化到二次方级别。但是为什么红呢?

手模一份样例,可以发现 kmp[i+j]-kmp[i-j-1] 的统计结果并不靠谱。有的模式串末尾确实在回文子串内,但是开头不在啊,那么这个模式串就不能算在回文子串内了。

所以如果模式串长度为 m ,那么要统计的范围就应该是 kmp[i+j]-kmp[i-j+m-1] ,如果这个范围不合法那么就应该是 0

红的问题暂时解决了,那么黑的问题怎么办呢?

观察,可以发现,对于每个 i ,他的答案的正贡献就是 j=ii+mnchri1kmpj,负贡献就是 j=imnchri+mi1kmpj 。那么我们对前缀和得出来的 kmp 数组再次进行前缀和,就可以线性时间内求出答案。

for(int i=1;i<=n;++i) ans+=(kmp[i+mnchr[i]-1]-kmp[i-1])-(kmp[i-1]-kmp[i-mnchr[i]+m-1]);

那么边界问题怎么解决呢?如果一些位置 imnchri+m>i1 的话这样计算也是错误的,然而这条式子中并没有很好的方法能把他剔除掉。

在想到边界问题之后就发现上面正负贡献的式子貌似有点问题。不使用第二次前缀和优化的时候统计的是 kmp[i+j]-kmp[i-j+m-1] ,已经要求 i+j>ij+m1 ,因此也不是像我们本来想得那么天真。从另一个方面想上面的式子也是错误的:正贡献和负贡献的个数应该相同,而上面明显负贡献少了m 个。

那么我们只好推到重来,手模一下不用第二次前缀和优化时候的统计:

首先给定一个第一次前缀和做出来的 kmp 序列,每次给定 l=imnchri+m,r=i+mnchri1 ,然后每次操作给答案加上 kmprkmpl ,执行完之后 r--, l++;。执行直到 lr

那么其实也是分为正贡献和负贡献两部分,只是这两部分的计算有点难。

思维量小一点的话,搞一个函数 query(int a,int b) 专门统计 [a,b] 的答案。在里面就分类讨论。首先 int mid=(a+b)>>1;

因为两边同时收缩,因此不外乎下面三种情况:

  1. a 先到达 mid
    那么负贡献就是[a,mid],正贡献就是 (mid,b]
  2. b 先到达 mid
    负贡献 [a,mid),正贡献 [mid,b]
  3. 一起到达 mid
    剔除掉中间那个重复的之后,负贡献 [a,mid),正贡献 (mid,b]

于是乎得到了这样一个函数。

inline unsigned query(int a,int b) {
	if(a>b) return 0;
	--a;
	const int mid=(a+b)>>1;
	const int flag=(mid-a)-(b-mid); // 看那边离 mid 近
	if(flag==0) return (kmp[b]-kmp[mid])-(kmp[mid-1]-kmp[a-1]); // 一起到
	if(flag>0) return (kmp[b]-kmp[mid-1])-(kmp[mid-1]-kmp[a-1]); // b 先到
	if(flag<0) return (kmp[b]-kmp[mid])-(kmp[mid]-kmp[a-1]); // a 先到
	return 0; // 顺手写 return 0; 的好习惯(然而并没有什么用)
}
int main() {
    // do sth.
    for(int i=1;i<=n;++i) ans+=query(i-mnchr[i]+m,i+mnchr[i]-1);
    return 0;
}

想一下还有没有优化常数的方法。

首先读入优化很见仁见智,因此不加以赘述。

考虑一下真的每次我们都需要进行三个分支判断吗?不一定吧。

一起到是什么意思,就是 a,b 在走了相同的步数之后还差一步就重合了,那么他们两个必定奇偶性不同

如果其中一个先到,就是 a,b 在走了相同的步数之后相邻了,那么他们两个必定奇偶性相同

同时想一想,如果奇偶性相同,他们最后相邻的之后,必定是 a 先到达,因为 mid 是向下取整的,不可能 b 先到。

那么根据 a,b 奇偶性不同我们就可以定下来究竟用哪个式子了。

然后看一眼 ans+=query(i-mnchr[i]+m,i+mnchr[i]-1);

其中i-mnchr[i]i+mnchr[i] 的奇偶性必定相同,于是乎他们的奇偶性就取决于 m 的奇偶性了。

如果 m 是奇数,那么他们奇偶性相同。否则他们奇偶性不同。

然而两条式子只差一个 -1 ,因此一个绝妙的注意浮上心头。。。就是根据这个奇偶性来提前规划究竟用哪个式子。

然而由于神秘的东方力量,在加上这个优化之后更慢了(还莫名其妙紫了一个点)。所以这个优化。。。就算了吧。

(反正凭着读入优化也骗到了最优解

#include <cstdio>
#include <algorithm>

const int N=3e6;
int n,m;
char *s,*t,*p,input[6000021];
int f[N],mnchr[N];
unsigned kmp[N];

inline unsigned query(int a,int b) {
	if(a>b) return 0;
	--a;
	const int mid=(a+b)>>1;
	const int flag=(mid-a)-(b-mid);
	if(flag==0) return (kmp[b]-kmp[mid])-(kmp[mid-1]-kmp[a-1]);
	if(flag<0) return (kmp[b]-kmp[mid])-(kmp[mid]-kmp[a-1]);
	return 0;
}

int main() {
	fread(p=input,1,6000021,stdin);
	while(*p!=' ') n=(n<<1)+(n<<3)+(*(p++)^48);
	while(*(++p)!='\n') m=(m<<1)+(m<<3)+(*p^48);
	t=p; s=p+n+1;
	t[0]=-17; s[0]=0; s[m+1]=0;
	for(register int i=2,j=f[1]=0;i<=n;++i) {
		while(j and s[i]!=s[j+1]) j=f[j];
		if(s[i]==s[j+1]) f[i]=++j;
		else f[i]=j=0;
	}
	for(register int i=1,j=0;i<=n;++i) {
		while(j and s[j+1]!=t[i]) j=f[j];
		if(s[j+1]==t[i]) ++j;
		if(j==m) kmp[i]=1, j=f[j];
	}
	for(register int i=1;i<=n;++i) kmp[i]+=kmp[i-1];
	for(register int i=1;i<=n;++i) kmp[i]+=kmp[i-1];
	register int x=0,y=0;
	for(int i=1;i<=n;++i) {
		if(i<y) mnchr[i]=std::min(mnchr[(x<<1)-i],y-i);
		else mnchr[i]=1;
		while(t[i-mnchr[i]]==t[i+mnchr[i]]) ++mnchr[i];
		if(i+mnchr[i]>y) y=i+mnchr[x=i];
	}
	register unsigned ans=0;
	for(register int i=1;i<=n;++i) {
		ans+=query(i-mnchr[i]+m,i+mnchr[i]-1);
	}
	printf("%u\n",ans);
	return 0;
}
posted @   IdanSuce  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示