P4173 残缺的字符串(带通配符的字符串匹配,FFT)

题意:

很久很久以前,在你刚刚学习字符串匹配的时候,有两个仅包含小写字母的字符串A和B,其中A串长度为m,B串长度为n。可当你现在再次碰到这两个串时,这两个串已经老化了,每个串都有不同程度的残缺。

你想对这两个串重新进行匹配,其中A为模板串,那么现在问题来了,请回答,对于B的每一个位置i,从这个位置开始连续m个字符形成的子串是否可能与A串完全匹配?

数据范围:n,m<=3e5

解法:

先介绍FFT进行字符串匹配的原理:

假设A和B是数字,那么A=B可以等价为A-B=0,

当A和B是字符串的时候,A=B似乎可以变为: ∑ i = 0 n − 1 ( A [ i ] − b [ i ] ) = 0 \sum_{i=0}^{n-1}(A[i]-b[i])=0 i=0n1(A[i]b[i])=0

但是求和存在正负相消,因此将每个位置的计算变成绝对值: ∑ i = 0 n − 1 ∣ A [ i ] − B [ i ] ∣ = 0 \sum_{i=0}^{n-1}|A[i]-B[i]|=0 i=0n1A[i]B[i]=0

绝对值不好算,可以平方一下把式子变成: ∑ i = 0 n − 1 ( A [ i ] − B [ i ] ) 2 = 0 \sum_{i=0}^{n-1}(A[i]-B[i])^2=0 i=0n1(A[i]B[i])2=0

展开变为: ∑ i = 0 n − 1 ( A [ i ] 2 + B [ i ] 2 ) − 2 ∑ i = 0 n − 1 A [ i ] ∗ B [ i ] = 0 \sum_{i=0}^{n-1}(A[i]^2+B[i]^2)-2\sum_{i=0}^{n-1}A[i]*B[i]=0 i=0n1(A[i]2+B[i]2)2i=0n1A[i]B[i]=0

左边的式子可以预处理前缀和算,

右边的式子可以通过将B串翻转为B’,变为: 2 ∑ i = 0 n − 1 A [ i ] ∗ B ′ [ n − 1 − i ] 2\sum_{i=0}^{n-1}A[i]*B'[n-1-i] 2i=0n1A[i]B[n1i]

是卷积形式,可以用FFT计算,复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

-----分割线-----

观察复杂度,显然用KMP算法匹配效率更高,那用FFT匹配有啥用呢?

当字符串中存在通配符时,KMP算法没办法进行匹配,这时候就得用FFT了。

对于A[i]和B[i],如果他们能匹配,那么一定满足下列条件之一:

1.A[i]是通配符

2.B[i]是通配符

3.A[i]=B[i]

我们原本要满足的是 ( A [ i ] − B [ i ] ) 2 = 0 (A[i]-B[i])^2=0 (A[i]B[i])2=0,即条件三,

现在多了通配符的条件,可以设通配符为0,

那么只需要把式子变为 ( A [ i ] − B [ i ] ) 2 ∗ A [ i ] ∗ B [ i ] = 0 (A[i]-B[i])^2*A[i]*B[i]=0 (A[i]B[i])2A[i]B[i]=0即可,

用0乘任何数都等于0巧妙的将三个条件逻辑与了起来。

那么串A=串B的式子就变为: ∑ i = 0 n − 1 ( A [ i ] − B [ i ] ) 2 ∗ A [ i ] ∗ B [ i ] \sum_{i=0}^{n-1}(A[i]-B[i])^2*A[i]*B[i] i=0n1(A[i]B[i])2A[i]B[i]

将式子展开变为: ∑ i = 0 n − 1 ( A [ i ] 3 ∗ B [ i ] ) − 2 ∑ i = 0 n − 1 ( A [ i ] 2 ∗ B [ i ] 2 ) + ∑ i = 0 n − 1 ( A [ i ] ∗ B [ i ] 3 ) \sum_{i=0}^{n-1}(A[i]^3*B[i])-2\sum_{i=0}^{n-1}(A[i]^2*B[i]^2)+\sum_{i=0}^{n-1}(A[i]*B[i]^3) i=0n1(A[i]3B[i])2i=0n1(A[i]2B[i]2)+i=0n1(A[i]B[i]3)

翻转B串变为: ∑ i = 0 n − 1 ( A [ i ] 3 ∗ B ′ [ n − 1 − i ] ) − 2 ∑ i = 0 n − 1 ( A [ i ] 2 ∗ B ′ [ n − 1 − i ] 2 ) + ∑ i = 0 n − 1 ( A [ i ] ∗ B ′ [ n − 1 − i ] 3 ) \sum_{i=0}^{n-1}(A[i]^3*B'[n-1-i])-2\sum_{i=0}^{n-1}(A[i]^2*B'[n-1-i]^2)+\sum_{i=0}^{n-1}(A[i]*B'[n-1-i]^3) i=0n1(A[i]3B[n1i])2i=0n1(A[i]2B[n1i]2)+i=0n1(A[i]B[n1i]3)

C ( n ) = ∑ i = 0 n ( A [ i ] 3 ∗ B ′ [ n − i ] ) − 2 ∑ i = 0 n ( A [ i ] 2 ∗ B ′ [ n − i ] 2 ) + ∑ i = 0 n ( A [ i ] ∗ B ′ [ n − i ] 3 ) C(n)=\sum_{i=0}^{n}(A[i]^3*B'[n-i])-2\sum_{i=0}^{n}(A[i]^2*B'[n-i]^2)+\sum_{i=0}^{n}(A[i]*B'[n-i]^3) C(n)=i=0n(A[i]3B[ni])2i=0n(A[i]2B[ni]2)+i=0n(A[i]B[ni]3)

FFT计算出C[],然后逐个判断C[i]是否为0即可。

要用eps判断,因为是浮点数。

-----分割线-----

一开始没想懂为什么只需要判断C(i)是否为0,c(i-m)不用管,手玩了一组数据就懂了:

在这里插入图片描述
翻转,空余处补零,卷积的时候前面部分会和0消掉,所以直接判断C(i)就行了。
在这里插入图片描述

-----分割线-----

学自:https://www.luogu.com.cn/blog/cqbzllsw/bzoj-4259-can-que-di-zi-fu-chuan

code:
#include<bits/stdc++.h>
using namespace std;
const double P=acos(-1.0);
struct CC{//复数
    double x,y;
    CC(double xx=0,double yy=0){x=xx,y=yy;}
    CC operator+(const CC &a)const{return CC(x+a.x,y+a.y);}
    CC operator-(const CC &a)const{return CC(x-a.x,y-a.y);}
    CC operator*(const CC &a)const{return CC(x*a.x-y*a.y,x*a.y+y*a.x);}
    CC operator*(const double a)const{return CC(x*a,y*a);}
};
void change(CC y[],int len){
    for(int i=1,j=len/2;i<len-1;i++){
        if(i<j)swap(y[i],y[j]);
        int k=len/2;
        while(j>=k){
            j-=k;
            k/=2;
        }
        if(j<k)j+=k;
    }
}
void fft(CC y[],int len,int on){//on为1或者-1,-1的时候表示逆变换
    change(y,len);
    for(int h=2;h<=len;h<<=1){
        CC wn(cos(-on*2*P/h),sin(-on*2*P/h));
        for(int j=0;j<len;j+=h){
            CC w(1,0);
            for(int k=j;k<j+h/2;k++){
                CC u=y[k];
                CC t=w*y[k+h/2];
                y[k]=u+t;
                y[k+h/2]=u-t;
                w=w*wn;
            }
        }
    }
    if(on==-1){
        for(int i=0;i<len;i++){
            y[i].x/=len;
        }
    }
}
//
const int maxm=2e6+5;
char s[maxm];
char t[maxm];
int a[maxm];
int b[maxm];
CC A[maxm];
CC B[maxm];
CC C[maxm];
int n,m;
signed main(){
    scanf("%d%d",&m,&n);
    scanf("%s%s",t,s);
    for(int i=0;i<n;i++)a[i]=(s[i]=='*'?0:s[i]-'a'+1);
    for(int i=0;i<m;i++)b[i]=(t[i]=='*'?0:t[i]-'a'+1);
    reverse(b,b+m);
    //
    int len=1;
    while(len<n*2)len<<=1;
    //
    for(int i=0;i<n;i++)A[i]=CC(a[i]*a[i]*a[i],0);
    for(int i=n;i<len;i++)A[i]=CC(0,0);
    for(int i=0;i<m;i++)B[i]=CC(b[i],0);
    for(int i=m;i<len;i++)B[i]=CC(0,0);
    fft(A,len,1);
    fft(B,len,1);
    for(int i=0;i<len;i++)C[i]=C[i]+A[i]*B[i];
    //
    for(int i=0;i<n;i++)A[i]=CC(a[i]*a[i],0);
    for(int i=n;i<len;i++)A[i]=CC(0,0);
    for(int i=0;i<m;i++)B[i]=CC(b[i]*b[i],0);
    for(int i=m;i<len;i++)B[i]=CC(0,0);
    fft(A,len,1);
    fft(B,len,1);
    for(int i=0;i<len;i++)C[i]=C[i]-A[i]*B[i]*2.0;
    //
    for(int i=0;i<n;i++)A[i]=CC(a[i],0);
    for(int i=n;i<len;i++)A[i]=CC(0,0);
    for(int i=0;i<m;i++)B[i]=CC(b[i]*b[i]*b[i],0);
    for(int i=m;i<len;i++)B[i]=CC(0,0);
    fft(A,len,1);
    fft(B,len,1);
    for(int i=0;i<len;i++)C[i]=C[i]+A[i]*B[i];
    //
    fft(C,len,-1);
    vector<int>ans;
    for(int i=m-1;i<n;i++){
        if(fabs(C[i].x)<0.5){
            ans.push_back(i-m+2);
        }
    }
    printf("%d\n",ans.size());
    for(auto i:ans){
        printf("%d ",i);
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值