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

xiaoxiao2021-02-28  13

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

【题目】 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; } }

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

i01234567891011s2[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.

转载请注明原文地址: https://www.6miu.com/read-450324.html

最新回复(0)