【马拉车】【kmp】【前缀和】LGP6216 回文匹配
题意
给定文本串 \(T\) ,模式串 \(S\) ,求 \(S\) 在 \(T\) 的所有奇数回文子串中出现的次数。答案取 \(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\) ,他的答案的正贡献就是 \(\sum_{j=i}^{i+mnchr_i-1}kmp_j\),负贡献就是 \(\sum_{j=i-mnchr_i+m}^{i-1}kmp_j\) 。那么我们对前缀和得出来的 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]);
那么边界问题怎么解决呢?如果一些位置 \(i-mnchr_i+m>i-1\) 的话这样计算也是错误的,然而这条式子中并没有很好的方法能把他剔除掉。
在想到边界问题之后就发现上面正负贡献的式子貌似有点问题。不使用第二次前缀和优化的时候统计的是 kmp[i+j]-kmp[i-j+m-1] ,已经要求 \(i+j>i-j+m-1\) ,因此也不是像我们本来想得那么天真。从另一个方面想上面的式子也是错误的:正贡献和负贡献的个数应该相同,而上面明显负贡献少了\(m\) 个。
那么我们只好推到重来,手模一下不用第二次前缀和优化时候的统计:
首先给定一个第一次前缀和做出来的 kmp 序列,每次给定 \(l=i-mnchr_i+m, r=i+mnchr_i-1\) ,然后每次操作给答案加上 \(kmp_r-kmp_l\) ,执行完之后 r--, l++;。执行直到 \(l\geq r\)。
那么其实也是分为正贡献和负贡献两部分,只是这两部分的计算有点难。
思维量小一点的话,搞一个函数 query(int a,int b) 专门统计 \([a,b]\) 的答案。在里面就分类讨论。首先 int mid=(a+b)>>1;
因为两边同时收缩,因此不外乎下面三种情况:
- \(a\) 先到达 \(mid\)
那么负贡献就是\([a,mid]\),正贡献就是 \((mid,b]\) - \(b\) 先到达 \(mid\)
负贡献 \([a,mid)\),正贡献 \([mid,b]\) - 一起到达 \(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;
}

浙公网安备 33010602011771号