*(ノ゚▽゚)ノ对方不想和你说话并且向你扔出了一个题。
【题目】 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;
}
}
建议自己脑跑一遍以加深理解。
栗子:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
s2[i] | a | a | b | a | a | b | a | a | c | a | b | 无 |
fail[i] | 0 | 0 | 1 | 0 | 1 | 2 | 3 | 4 | 5 | 0 | 1 | 0 |
网上的博客里还有另一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.