<学习笔记>KMP(MP)算法

本文介绍了KMP算法,用于解决字符串匹配问题。通过讲解KMP算法的基本思想、时间复杂度以及next数组的生成,帮助读者理解算法的核心。并提供了一个具体的题目实例,展示如何使用KMP算法解决字符串匹配,并给出了相关代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

*(ノ゚▽゚)ノ对方不想和你说话并且向你扔出了一个题。

【题目】 luogu 3375 KMP字符串匹配

题目描述

如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。

为了减少骗分的情况,接下来还要输出子串的前缀数组next。

(如果你不知道这是什么意思也不要问,去百度搜[kmp算法]学习一下就知道了。)

输入输出格式

输入格式:
第一行为一个字符串,即为s1(仅包含大写字母)

第二行为一个字符串,即为s2(仅包含大写字母)

输出格式:

若干行,每行包含一个整数,表示s2在s1中出现的位置

接下来1行,包括length(s2)个整数,表示前缀数组next[i]的值。

输入输出样例

输入样例#1:
ABABABC
ABA
输出样例#1:
1
3
0 0 1

说明

时空限制:1000ms,128M

数据规模:

设s1长度为N,s2长度为M

对于30%的数据:N<=15,M<=5

对于70%的数据:N<=10000,M<=100

对于100%的数据:N<=1000000,M<=1000

Ps:由于我打的字符串下标是以0为开头的,学的也是以0为开头的,将就看吧。。。

对于这个题,让我们先抛开kmp,想想可以怎么解_(:з」∠)_
首先想到的是暴力匹配。
对于s1和s2,暴力枚举每一个点并进行匹配,由于每次进行匹配时时间复杂度为O(m),至少需要扫(n-m)个点,时间复杂度近似O(nm),TLE!
优化:每次匹配时如果有不同的就直接跳过。

依旧会T…orz

那么…(ノ゚▽゚)ノ kmp…

KMP算法简介

kmp算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。(摘自百度百科)

O(m): 对s2的预处理
O(n) : 扫一遍s1,完成匹配

当匹配的过程中发现s1[i]与s2[j]不同时
如 匹配到c和a发现他们不同:

这里写图片描述

暴力会把aabaabaa往后移一位重新比较。
而kmp算法会因为c之前的已经比较过,我们已经知道s1前面的部分为aabaaba了,显然后移一位不可能匹配成功,后移两位也不可能匹配成功,而后移三位是有可能的。所以kmp算法会直接后移三位后再比对c和s2的第5个字符。

由于s2已经给出,所以我们可以预先处理出当s2[i]匹配失败时转移后应继续判断的位置(s2可能能继续匹配的位置 or s2后移的位数)。 这里用fail数组表示。
如上图中的 fail[7]=4;

可以写出kmp的代码为

void Kmp()
{
    n=strlen(s1),m=strlen(s2);
    Done_fail(); 
    for(int i=0,j=0;i<n;++i) // j既可以理解为下标,也可以理解为当前已经匹配了j个字符(0开头的好处2333)
    {
        while(j&&s1[i]!=s2[j]) j=fail[j]; // 如果不匹配,就把j往前跳(s2往后移),直到j=0或可以匹配为止
        if(s1[i]==s2[j]) ++j; // 可以匹配 j++;
        if(j==m) Ans[++ans]=i-m+1,j=fail[j];  // 匹配成功,记录下出现的位置并更新j
    }
}

难点:

fail数组的处理:

递推求法:

void Done()
{
    fail[0]=fail[1]=0; 
    for(int i=1,k=0;i<m;++i)
    {
        k=fail[i]; // 已经匹配了i(0~i-1)个字符的fail值,也就是i的fail值,此时我们并不知道s2[i]是否已经/可以匹配,通过判断s2[i],我们用fail[i]来更新fail[i+1]。
        while(k&&s2[i]!=s2[k]) k=fail[k];// 如果s2[i]!=s2[k](s2[k]为s2可能能继续匹配的字符),说明s2[i+1]如果失配了,我们并不能直接转移到s2[k+1]尝试匹配。所以我们就像上面的kmp代码一样,不断往前跳,直到找到一个可以匹配的地方为止。(就如同字符串失配了一样)
        fail[i+1]=(s2[i]==s2[k])?k+1:0;
    }
}

建议自己脑跑一遍以加深理解。
栗子:

i01234567891011
s2[i]aabaabaacab
fail[i]001012345010

网上的博客里还有另一1种理解方法,用next数组表示,统计s2”前缀”和”后缀”的最长的共有元素的长度,next[i]表示s2中前i个字符的”前缀”和”后缀”的最长的共有元素的长度,如 ABCDABBD 对应的next 为 0,0,0,0,1,2,0,0 。所以next[i-1]可以直接用作下标为i的字符失配时应转移到的位置。也就是说fail是next统一往后移了一位。其实思想是一样的。(next应该会更好懂一些)

摘自网上的next代码。

void Done_next()
{
    next[0] = 0;
    for (int i = 1,k = 0; i < m; ++i) // k为最大长度
    {
        while(k > 0 && s2[i] != s2[k])
             k = next[k-1];       
        if (s2[i] == s2[k])
        {
            k++;
        }
        next[i] = k;
    }
}

那么回到原题,发现只是个模板题,直接上代码好了(:3_ヽ)_

注:原题的字符串是以1为开头的,另外求的也是next数组

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
using namespace std;

int n,m,ans;
int fail[1010],Ans[1000010];
char a[1000010],b[1010];

void Done()
{
    fail[0]=fail[1]=0;
    for(int i=1,k=0;i<m;++i)
    {
        while(k&&b[i]!=b[k]) k=fail[k];
        fail[i+1]=b[i]==b[k]?++k:0;
    }
}
void Kmp()
{
    n=strlen(a),m=strlen(b);
    Done();
    for(int i=0,j=0;i<n;++i)
    {
        while(j&&a[i]!=b[j]) j=fail[j];
        if(a[i]==b[j]) ++j;
        if(j==m) Ans[++ans]=i-m+1,j=fail[j];
    }
}
int main()
{
    cin>>a>>b;
    Kmp();
    //cout<<ans<<'\n';  
    for(int i=1;i<=ans;++i)
      cout<<Ans[i]+1<<'\n';  // 以一为开头
    for(int i=1;i<=m;++i)  // 往前移一位
    cout<<fail[i]<<' ';
    cout<<'\n';
    return 0;
} 

ヾノ≧∀≦)o βyё βyё..

The end.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值