• 洛谷P3375 【模板】KMP字符串匹配


    题目传送门:戳我进入

    KMP算法是用来处理字符串匹配的问题的,也就是给你两个字符串,你需要回答B串是否是A串的子串,B串在A串中出现了几次,B串在A串中出现的位置等问题。

    KMP算法的意义在于,如果你在洛谷上发了一些话,kkksc03就可以根据KMP算法查找你是否说了一些不和谐的字,并且屏蔽掉你的句子里的不和谐的话(比如cxk鸡你太美就会被屏蔽成cxk****),还会根据你句子中出现不和谐的字眼的次数对你进行处罚

    举个栗子:A:GCAKIOI      B:GC     ,那么我们称B串是A串的子串

    我们称等待匹配的A串为主串,用来匹配的B串为模式串。

    一般的朴素做法就是枚举B串的第一个字母在A串中出现的位置并判断是否适合,而这种做法的时间复杂度是O(mn)的,当你处理一篇较长文章的时候显然就会超时。

    我们会发现在字符串匹配的过程中,绝大多数的尝试都会失败,那么有没有一种算法能够利用这些失败的信息呢?

    KMP算法就是

    KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的

    设主串(以下称为T)

    设模式串(以下称为W)

    用暴力算法匹配字符串过程中,我们会把T[0] 跟 W[0] 匹配,如果相同则匹配下一个字符,直到出现不相同的情况,此时我们会丢弃前面的匹配信息,然后把T[1] 跟 W[0]匹配,循环进行,直到主串结束,或者出现匹配成功的情况。这种丢弃前面的匹配信息的方法,极大地降低了匹配效率。

    我们来看一看KMP是怎么工作的

    在KMP算法中,对于每一个模式串我们会事先计算出模式串的内部匹配信息(也就是说这个东西只和模式串有关,可以预处理,这个处理我们后面会提到),在匹配失败时最大的移动模式串,以减少匹配次数。

    比如,在简单的一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配。右移的距离在KMP算法中是如此计算的:在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使它们重叠。

    我们用两个指针i和j分别表示A[i-j+1......i]和B[1......j]完全相等,也就是说i是不断增加的,并且随着i的增加,j也相应的变化,并且j满足以A[j]结尾的长度为j的字符串正好匹配B串的前j个字符,现在需要看A[i+1]和B[j+1]的关系

    • 当A[i+1]=B[j+1]时,我们将i和j各增加1
    • 否则,我们减小j的值,使得A[i-j+1......i]和B[1......j]保持匹配并尝试匹配新的A[i+1]和B[j+1]

    举个栗子:

    T: a b a b a b a a b a b a c b

    W:a b a b a c b

    当i=j=5时,此时T[6]!=W[6],这表明此时j不能等于5了,这个时候我们要改变j的值,使得W[1...j]中的前j'个字母与后j'个字母相同,因为这样j变成j'后(也就是将W右移j'个长度)才能继续保持i和j的性质。这个j'显然越大越好。在这里W[1...5]是匹配的,我们发现当ababa的前三个字母和后三个字母都是aba,所以j'最大也就是3,此时情况是这样

    T: a b a b a b a a b a b a c b

    W:      a b a b a c b

    那么此时i=5,j=3,我们又发现T[6]与W[4]是相等的,然后T[7]与W[5]是相等的(这里是两步)

    所以现在是这种情况:i=7,j=5

    T: a b a b a b a a b a b a c b

    W:      a b a b a c b

    这个时候又出现了T[8]!=W[6]的情况,于是我们继续操作。由于刚才已经求出来了当j=5时,j'=3,所以我们就可以直接用了(通过这里我们也可以发现j'是多少和主串没有什么关系,只和模式串有关系)

    于是又变成了这样

    T: a b a b a b a a b a b a c b

    W:            a b a b a c b

    这时,新的j=3依然不能满足A[i+1]=B[j+1],所以我们还需要取j'

    我们发现当j=3时aba的第一个字母和最后一个字母都是a,所以这时j'=1

    新的情况:

    T: a b a b a b a a b a b a c b

    W:                  a b a b a c b

    仍然不满足,这样的话j需要减小到j'就是0(我们规定当j=1时,j'=0)

    T: a b a b a b a a b a b a c b

    W:                     a b a b a c b

    终于,T[8]=B[1],i变为8,j变为1,我们一位一位往后,发现都是相等的,最后当j=7还满足条件时,我们就可以下结论:W是T的子串,并且还可以找到子串在主串中的位置(i+1-m+1,因为下标从0开始)

    这一部分的代码其实很短,因为用了for循环

    inline void kmp()
    {
        int j=0;
        for(int i=0;i<n;i++)
        {
            while(j>0&&b[j+1]!=a[i+1]) j=nxt[j];
            if(b[j+1]==a[i+1]) j++;
            if(j==m) 
            {
                printf("%d
    ",i+1-m+1);
                j=nxt[j]; 
        //当输出第一个位置时 直接break掉 
        //当输出所有位置时 j=nxt[j]; 
        //当输出区间不重叠的位置时 j=0 
            }
        }
    }

    这里就有一个问题:为什么时间复杂度是线性的?

    我们从上述的j值入手,因为每执行一次while循环都会使j值减小(但不能到负数),之后j最多+1,因此整个过程中最多加了n个1.于是j最多只有n个机会减小。这告诉我们,while循环最多执行了n次,时间复杂度平摊到for循环上后,一次for循环的复杂度是O(1),那么总的时间复杂度就是O(n)的(n是主串长度)。这样的分析对于下文的预处理来说同样有效,也可以得到预处理的时间复杂度是O(m)(m是模式串长度)

    接下来是预处理

    预处理并不需要按照定义写成O(m2)甚至O(m3),窝们可以通过nxt[1],nxt[2]....nxt[n-1]来求得nxt[n]的值

    举个栗子

     W :a b a b a c b

    nxt:0 0 1 2 ??

    假如我们有一个串,并且已经知道了nxt[1~4]那么如何求nxt[5]和nxt[6]呢?

    我们发现,由于nxt[4]=2,所以w[1~2]=w[3~4],求nxt[5]的时候,我们发现w[3]=w[5],也就是说我们可以在原来的基础上+1,从而得到更长的相同前后缀,此时nxt[5]=nxt[4]+1=3

    W :a b a b a c b

    nxt:0 0 1 2 3?

    那么nxt[6]是否也是nxt[5]+1呢?显然不是,因为w[nxt[5]+1]!=w[6],那么此时我们可以考虑退一步,看看nxt[6]是否可以由nxe[5]的情况所包含的子串得到,即是否nxt[6]=nxt[nxt[5]]+1?

    事实上,这样一直推下去也不行,于是我们知道nxt[6]=0

    那么预处理的代码就是这样的

    inline void pre()
    {
        nxt[1]=0;//定义nxt[1]=0 
        int j=0;
        rep(i,1,m-1)
        {
            while(j>0&&b[j+1]!=b[i+1]) j=nxt[j];
            //不能继续匹配并且j还没有减到0,就退一步 
            if(b[j+1]==b[i+1]) j++;
            //如果能匹配,就j++ 
            nxt[i+1]=j;//给下一个赋值
        }
    }

    完整的代码:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<queue>
    #include<algorithm>
    #include<iomanip>
    using namespace std;
    #define rep(i,a,n) for(int i=a;i<=n;i++)
    #define per(i,n,a) for(int i=n;i>=a;i--)
    typedef long long ll;
    ll read()
    {
        ll ans=0;
        char last=' ',ch=getchar();
        while(ch<'0'||ch>'9') last=ch,ch=getchar();
        while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar();
        if(last=='-') ans=-ans;
        return ans;
    }
    
    char a[1000005],b[1000005];
    int nxt[1000005],n,m;
    
    inline void pre()
    {
        nxt[1]=0;
        int j=0;
        rep(i,1,m-1)
        {
            while(j>0&&b[j+1]!=b[i+1]) j=nxt[j]; 
            if(b[j+1]==b[i+1]) j++;
            nxt[i+1]=j; 
        }
    }
    
    inline void kmp()
    {
        int j=0;
        for(int i=0;i<n;i++)
        {
            while(j>0&&b[j+1]!=a[i+1]) j=nxt[j];
            if(b[j+1]==a[i+1]) j++;
            if(j==m) 
            {
                printf("%d
    ",i+1-m+1);
                j=nxt[j]; 
            }
        }
        rep(i,1,m) printf("%d ",nxt[i]);
        
    }
    
    int main()
    {
        scanf("%s%s",a+1,b+1);
        n=strlen(a+1),m=strlen(b+1);
        pre();
        kmp();
        return 0;
    }
  • 相关阅读:
    feign远程调用问题
    java8--stream
    feign业务组件远程请求 /oauth/token
    redis实现自增序列
    MySQL数据库 相关知识点
    netty
    spring的启动流程及bean的生命周期
    MethodHandleVS反射
    并发与并行
    关于注解的思考
  • 原文地址:https://www.cnblogs.com/lcezych/p/11002026.html
Copyright © 2020-2023  润新知