【马拉车】【kmp】【前缀和】LGP6216 回文匹配
题意
给定文本串 ,模式串 ,求 在 的所有奇数回文子串中出现的次数。答案取 位无符号整数自然溢出的结果。时空复杂度要求线性。
思路
奇数回文子串,马拉车狂喜,都不用进行字符串处理。
出现次数,也就是字符串匹配,记录下出现的位置,考虑 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]
,如果这个范围不合法那么就应该是 。
红的问题暂时解决了,那么黑的问题怎么办呢?
观察,可以发现,对于每个 ,他的答案的正贡献就是 ,负贡献就是 。那么我们对前缀和得出来的 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]);
那么边界问题怎么解决呢?如果一些位置 的话这样计算也是错误的,然而这条式子中并没有很好的方法能把他剔除掉。
在想到边界问题之后就发现上面正负贡献的式子貌似有点问题。不使用第二次前缀和优化的时候统计的是 kmp[i+j]-kmp[i-j+m-1]
,已经要求 ,因此也不是像我们本来想得那么天真。从另一个方面想上面的式子也是错误的:正贡献和负贡献的个数应该相同,而上面明显负贡献少了 个。
那么我们只好推到重来,手模一下不用第二次前缀和优化时候的统计:
首先给定一个第一次前缀和做出来的 kmp
序列,每次给定 ,然后每次操作给答案加上 ,执行完之后 r--, l++;
。执行直到 。
那么其实也是分为正贡献和负贡献两部分,只是这两部分的计算有点难。
思维量小一点的话,搞一个函数 query(int a,int b)
专门统计 的答案。在里面就分类讨论。首先 int mid=(a+b)>>1;
因为两边同时收缩,因此不外乎下面三种情况:
- 先到达
那么负贡献就是,正贡献就是 - 先到达
负贡献 ,正贡献 - 一起到达
剔除掉中间那个重复的之后,负贡献 ,正贡献
于是乎得到了这样一个函数。
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;
}
想一下还有没有优化常数的方法。
首先读入优化很见仁见智,因此不加以赘述。
考虑一下真的每次我们都需要进行三个分支判断吗?不一定吧。
一起到是什么意思,就是 在走了相同的步数之后还差一步就重合了,那么他们两个必定奇偶性不同 。
如果其中一个先到,就是 在走了相同的步数之后相邻了,那么他们两个必定奇偶性相同 。
同时想一想,如果奇偶性相同,他们最后相邻的之后,必定是 先到达,因为 是向下取整的,不可能 先到。
那么根据 奇偶性不同我们就可以定下来究竟用哪个式子了。
然后看一眼 ans+=query(i-mnchr[i]+m,i+mnchr[i]-1);
其中i-mnchr[i]
和 i+mnchr[i]
的奇偶性必定相同,于是乎他们的奇偶性就取决于 的奇偶性了。
如果 是奇数,那么他们奇偶性相同。否则他们奇偶性不同。
然而两条式子只差一个 -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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具