题解 P4173 【残缺的字符串】
这是一道通配符匹配模板题。对于通配符的匹配,本人发表拙作如下。
浅谈FFT在字符串匹配中的应用
背景引入
字符串匹配,是OI中的一个古老为传统的问题。常见的匹配问题有:单模式串匹配、多模式串匹配、字串匹配。而对应的算法是KMP、AC自动机、后缀自动机。而我们今天要谈的问题,是单模式串匹配问题的一种:带通配符的单模式串匹配。
为了逐步解决这个问题,我们从普通的单模式串匹配开始说起。
普通的单模式串匹配
问题概述:给定模式串A(长度为m)、文本串B(长度为n),需要求出所有位置p,满足B串从第p个字符开始的连续m个字符,与A串完全相同
KMP是这一类问题的优秀算法,理论复杂度是线性的。但今天我们要用FFT来解决这个问题。千万不要因为FFT的复杂度高于KMP而忽略这一段,因为这一段内容对接下来我们要解决的问题而言,非常重要。
为了使问题更便于研究,我们约定所有字符串的下标从0开始
我们定义字符串的匹配函数C(x,y)=A(x)-B(y),那么我们可以形式化地定义“匹配”:若C(x,y)=0,则称A的第x个字符和B的第y个字符匹配。再定义完全匹配函数
但有没有觉得这个完全匹配函数有什么问题?似乎,根据完全匹配函数,“ab”与“ba”是匹配的???问题出在我们定义的匹配函数身上。匹配函数的确可以判断某一位是否匹配,但加了一个求和符号,一切都变了,只要两个串所含的字符是一样的,不管排列如何,完全匹配函数都会返回0值。
而这一切的罪魁祸首就是匹配函数的正负号!我们现在要做的是:定义一个更好的匹配函数,只要两个字符串某一位的匹配函数非零,完全匹配函数也不可能为零。不难想到给匹配函数加一个绝对值符号。但这样似乎就只能O(nm)暴力计算,没有更好的优化方法了。于是我们给匹配函数加上一个平方符号。此时完全匹配函数就是
这样还是没什么优化方法。我们不妨翻转A串,将翻转后的结果设为S。形式化地,S(x)=A(m-x-1)。据此不难推出A(x)-S(m-x-1)。于是完全匹配函数可以写成
大力展开这个函数:
再将求和符号拆开:
第一项是一个定值,可以O(m)预处理出来。第二项可以O(n)预处理前缀和。现在问题就在第三项。第三项中,两个小括号里面的东西加起来,似乎能抵消很多东西?居然……刚好等于x?这是巧合吗?当然不是,明明是我干出来的。所以,第三项是不是可以写成
那么,我们设
于是就有
观察这个g函数,发现它不就是我们熟悉的卷积形式吗?而且还是最常规的卷积——多项式乘法!于是可以用FFT计算出g函数了!然后再代入计算一下,就可以O(n)得到所有P(x)的值了!
说了那么多,其实代码很短的。下面是核心代码。FFT相信大家都会,就不放出来了
void FFT_Match(char *s1,char *s2,int m,int n)
{
for(int i=0;i<m;i++) A[i].r=s1[i]-'a'+1;
for(int i=0;i<n;i++) B[i].r=s2[i]-'a'+1;
reverse(A,A+m);double T=0;
for(int i=0;i<m;i++) T+=A[i].r*A[i].r;
f[0]=B[0].r*B[0].r;
for(int i=1;i<n;i++) f[i]=f[i-1]+B[i].r*B[i].r;
FFT(A,len,1);FFT(B,len,1);
for(int i=0;i<len;i++) g[i]=A[i]*B[i];
FFT(g,len,-1);
for(int x=m-1;x<n;x++)
{
double P=T+f[x]-f[x-m]-2*g[x].r;
if(fabs(P)<eps) printf("%d ",x-m+2);
}
}
带通配符的单模式串匹配
问题概述:给定模式串A(长度为m)、文本串B(长度为n),串的某些字符是“通配符”(通配符是一种可以与任意字符匹配的字符)。需要求出所有位置p,满足B串从第p个字符开始的连续m个字符,与A串完全相同
这个问题显然用KMP就无法解决了,我们还是考虑和上面类似的方法。那么我们回顾上面的普通串匹配过程,我们可以总结出思路大概是这样的:
-
定义匹配函数
-
定义完全匹配函数
-
快速计算每一位的完全匹配函数值
有了通配符,我们肯定需要重新定义一个匹配函数
我们设通配符的数值为0,定义匹配函数
还是老套路,将A串翻转,也就是设
然后完全匹配函数变成:
暴力展开:
发现所有的项,都满足两个小括号加起来等于x,所以全部写成卷积的形式:
这样只要做6次FFT外加1次IFFT就可以得到完全匹配函数每一位的值了
核心代码其实也不长,三次多项式乘法而已,常数略大
void FFT_match(char *s1,char *s2,int m,int n)
{
reverse(ss1,ss1+m);
for(int i=0;i<m;i++) A[i]=(s1[i]!='*')?(s1[i]-'a'+1):0;
for(int i=0;i<n;i++) B[i]=(s2[i]!='*')?(s2[i]-'a'+1):0;
for(int i=0;i<len;i++) a[i]=Comp(A[i]*A[i]*A[i],0),b[i]=Comp(B[i],0);
FFT(a,len,1);FFT(b,len,1);
for(int i=0;i<len;i++) P[i]=P[i]+a[i]*b[i];
for(int i=0;i<len;i++) a[i]=Comp(A[i],0),b[i]=Comp(B[i]*B[i]*B[i],0);
FFT(a,len,1);FFT(b,len,1);
for(int i=0;i<len;i++) P[i]=P[i]+a[i]*b[i];
for(int i=0;i<len;i++) a[i]=Comp(A[i]*A[i],0),b[i]=Comp(B[i]*B[i],0);
FFT(a,len,1);FFT(b,len,1);
for(int i=0;i<len;i++) P[i]=P[i]-a[i]*b[i]*Comp(2,0);
FFT(P,len,-1);
for(int i=m-1;i<n;i++) if(fabs(P[i].r)<=1e-7) printf("%d ",i-m+2);
}
本题直接套用上述代码即可。