KMP算法详解

不是很算详解吧

算法用途

KMP算法,俗称看毛片算法#(滑稽),用于求字符串的某一子串在该字符串中出现的次数(可能也不是其子串),也可以和其他算法结合起来用(比如和Trie树搞搞AC自动机什么的)

算法思想

预处理出一个next数组,实现快速跳转。其中next[i]存的是到第i个字符为止的字符串,其后缀与其前缀相等的最大长度。

感觉好难理解有木有!别急,我们来举个栗子。
比如说下面两个字符串:

s1串:bacacacbcacaca
s2串:cacaca

现在我们在s1串中找s2串。
第一次匹配:s1的b–>s2的c 发现并不一样,怎么办??把B串往后移一个呗!

s1串:bacacacbcacaca
s2串:0cacaca(空格打不出来 (╯﹏╰) 用0代替)

然而还是不一样嘛!好气哦!只能再移一个喽!

s1串:bacacacbcacaca
s2串:00cacaca

s1的c–>s2的c 终于一样啦!接下去几个亦如此!
但是最后一个不一样呢!又要一个一个移?这不是暴力嘛!
不不不,这次我们不一个一个移,而是两个一起移!为什么呢?之前我们提到过next数组(可以返回去看里面存的是什么)。然后我们再来看s2当前的串:cacac,其后缀cac与其前缀cac恰好相等,且此长度是最长的(当然自身不能算啦),移的长度便是:length(cacac)-length(cac)=2。

s1串:bacacac|bcacaca
s2串:00cacac|a

但是为什么这么移是可行的呢?我们看到,因为其后缀cac已经是比较过了,说明到这里为止都是匹配的,再把其相同的前缀拿过来,就省去了之前的一步一步移,以及无用的比较。
那有些人就要问了,如果是像ccccc这样的怎么办?不是会跳过去么?
我们能看到,ccccc的最长相同的前后缀为cccc(除去自己),其长度减去其最长相同前后缀的长度恰好为1,因此并无大碍。
于是移过后继续比较,直至找到目标即可。
算法主要代码:

node=0;
int ans=0;
for (int i=0;i<n;i++){
    while(node&&s2[node]!=s1[i])
        node=nxt[node-1];
    if (s2[node]==s1[i])
        node++;
    if (node==m){
        ans++;
        node=nxt[node-1];
    }
}

算法关键

整个算法的关键就是next数组。next数组要怎么求呢?
首先,根据它里面存的东西,我们可以判断:它肯定和寻找的那个串有关系,和作为寻找范围的串没有半毛钱关系。
然后,因为要找相同的前后缀,即要找到一个j,使得s[1->j]=s[i-j+1->i],这与之前的比较过程是非常相似的。
于是我们就可以推断出构造next数组的代码(讲的不是很清楚,请读者们自行体会其中的奥秘):

int node=0;
for (int i=1;i<m;i++){
    while(node&&s2[node]!=s2[i])
        node=nxt[node-1];
    if (s2[node]==s2[i])
        node++;
    nxt[i]=node;
}

模板题hihocoder1015洛谷3375(hihocoder上有更详细的解释,还不懂的小伙伴可以去看看)

代码

#include<cstdio>
#include<algorithm>
#include<cstring>
#define MAXN 1000000
using namespace std;
char s1[MAXN+5],s2[MAXN+5];
int t,n,m;
int nxt[MAXN+5];
int main(){
    scanf("%d",&t);
    while (t--){
        scanf("%s%s",s2,s1);
        n=strlen(s1); m=strlen(s2);
        memset(nxt,0,sizeof(nxt));
        int node=0;
        for (int i=1;i<m;i++){
            while(node&&s2[node]!=s2[i])
                node=nxt[node-1];
            if (s2[node]==s2[i])
                node++;
            nxt[i]=node;
        }
        node=0;
        int ans=0;
        for (int i=0;i<n;i++){
            while(node&&s2[node]!=s1[i])
                node=nxt[node-1];
            if (s2[node]==s1[i])
                node++;
            if (node==m){
                ans++;
                node=nxt[node-1];
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

算法拓展

最小循环节

若子串t通过不断重复可以构成s且t的长度最小,则t为s的最小循环节。
举个栗子:子串ad是字符串adadadad的最小循环节。

要怎么求最小循环节呢?

设s存在最小循环节t,那么t的长度即为strlen(s)-next[strlen(s)]。t即为s[next[strlen(s)]~strlen(s)]。(仔细体会下为什么)

不难发现,只要求出s的next数组就可以求出s的最小循环节。所以我们像KMP那样求一遍next数组就可以啦!

代码(并未评测过,有疑问请指出):

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
char s[100];
int nxt[100];
int main(){
    scanf("%s",s);
    int n=strlen(s);
    //求next数组
    int node=0;
    memset(nxt,0,sizeof(nxt));
    for (int i=1;i<n;i++){
        while (node&&s[i]!=s[node])
            node=nxt[node-1];
        if (s[i]==s[node])
            node++;
        nxt[i]=node;
    }
    //如果存在循环节
    if (nxt[n-1]&&n%(n-nxt[n-1])==0)
        printf("Yes\n");
    else{
        printf("No\n");
        return 0;
    }
    int l=n-nxt[n-1];//最小循环节长度
    for (int i=0;i<l;i++)
        printf("%c",s[i]);
    printf("\n");
    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值