KMP

前言

最近学了一下KMP,虽然我也不知道这是什么鬼,不过就其复杂程度,我觉得还是要写篇博客给自己一个清晰的思路。


相关

KMP算法是用来解决字符串s是否在t中出现过的问题,也就是匹配问题。
每一种算法都是从暴力开始的,所以我们先演示暴力算法:
将s串的头与t的每一位对齐进行比较,时间复杂度为O(ls*lt)。

bool compare(int j,int n)
{
    for(int i=0;i<n,i++) if(s[i]!=t[i+j]) return false;
    return true;
}
int main()
{
    scanf("%s",s);scanf("%t",t);int ls=strlen(s),lt=strlen(t);
    for(int i=0;i<lt-ls+1;i++) if(compare(i,ls)) ans++;
    return 0;
}

此时我们发现,每当失配(不匹配)后我们都将s串往后移一位(t往前移一位),这样十分费时,那有没有什么方法使我们可以向后移多位而不缺失答案呢,这就是KMP算法所要解决的。


快速后移

假设现在t[a]~t[b]与s[1]~s[x]匹配,但是t[b+1]与s[x+1]失配,如果要不止后移一位,我们要移动多少位才能不缺失信息呢?
首先,我们移动后肯定要保证s_[1]~s_[?]与s串已匹配部分的后半段,也就是c段是匹配的,否则这次移动就没意义,我们还需要再次移动。
同时为了不会缺失任何信息,我们还需保证c串的长度尽量的长。
这里写图片描述
我们继续观察,发现c串是s串已匹配部分的前缀,而c串匹配的是s串已匹配部分的后缀(如果我们将s串的每种已匹配方式看做是s的子串),那么求c串长度就变成了求s子串最长前缀等于后缀。


预处理

我们只需处理出s[1]~s[k],1<=k<=ls,的最长匹配前缀就行了(假定它储存在p[k]中),然后这也是一个匹配问题,绕了一圈又绕回来了,不过这个匹配问题是自己和自己匹配。
那这个p数组怎么用呢,还是上面那张图,当s[x+1]失配时,我们只需要将x转变为p[x],此时s[1]~s[p[x]]还是与t[a]~t[b]段的后缀匹配。
所以我们只要看看s[p[x]+1]与t[b+1]匹不匹配就行了,不匹配就继续找s[1]~s[p[x]]的最长前缀s[1]~s[p[p[x]]],然后不断得循环下去,直到找到为止。

同样的,我们的p数组也是用这种方法得出的:

void init(int ls);
{
    p[0]=-1;//初始化成-1是因为-1可以更轻松的跳循环,而且++j正好为0;
    for(int i=1,j=-1;i<=ls;i++)//j=-1是因为一个字符的最长前缀是没有的;
    {
        while(j>=0&&s[j+1]!=s[i]) j=p[j];//当j=-1时,前一个j为0,表示最长前缀的长度为0,这是下限,所以直接跳;
        //j=p[j]表示此时这个c串的后一个字符无法匹配,失配时跳到其最长匹配前缀继续匹配。
        p[i]=++j;
    }
}

我们可以知道j=p[j]时j是不断减小的,所以我们可以将p[j]从小到大递推就行了。


匹配

当我们预处理会做了之后,匹配就是和预处理一样的思想,只是有些变量改了一下而已。

int KMP(int ls,int lt)
{
    int ans=0;//字符串t中有几个字符串s;
    for(int i=1,j=0;i<=lt;i++)//j=0表示开始的匹配长度为0;
    {
        while(j>=0&&s[j+1]!=t[i]) j=p[j];
        j++;
        if(j==ls) ans++,j=p[j];//当匹配完成后,答案+1;
        //j=p[j]是允许s串在t中重叠出现,当改成j=0时,表示每找到一个s就要将s删除(不允许重叠计算)。
    }
    return ans;
}

此时我们的算法时间复杂度就变成了O(ls+lt),快了不知道多少。


完整代码

有T组数据,每组都有一个s串于t串,求s串在t串中出现了几次。

#include<bits/stdc++.h>
using namespace std;
int p[10005],T;
char s[10005],t[1000005];
void getp(int n)
{
    p[0]=-1;
    for(int i=1,j=-1;i<=n;i++)
    {
        while(j>=0&&s[j+1]!=s[i]) j=p[j];
        p[i]=++j;
    }
}
int KMP(int n,int m)
{
    int ans=0;
    for(int i=1,j=0;i<=m;i++)
    {
        while(j>=0&&s[j+1]!=t[i]) j=p[j];
        j++;
        if(j==n) ans++,j=p[j];
    }
    return ans;
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s",s+1);
        getp(strlen(s+1));
        scanf("%s",t+1);
        printf("%d\n",KMP(strlen(s+1),strlen(t+1)));
    }
    return 0;
}

玄学

PS:接下来就是作者的瞎哔哔了。

在p数组的基础上,我们还可以玩出一些好玩的性质:
这里写图片描述
易知,s[1]~s[p[i]]=s[t]~s[i],也就是x段等于y段,此时a段为s[p[i]]~s[i],是y段的后缀,那么等长的x段后缀b就会等于a,a+b段是y的后缀,c+d段是x的等长后缀,则a=b=c,且当n%(n-p[n])=0时,s[n]会形成一个周期。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值