赞
踩
kmp算法 分类:字符串
/*
这里推荐一位B站up主的视频,我的思路也借鉴于他。传送门:
理论篇:
帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili
代码实现篇:
帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili
*/
目录
kmp算法 通常的用途是:给你一段长字符串(母串),再给你一段短字符串(子串),让你找长字符串中有多少个短字符串出现。/或者让你在长字符串中,找满足条件的子串。
比如现在有一个母串和一个子串,让你找母串中子串第一次出现的位置。
文本串(母串)s1 aabaabaaf
模式串(子串)s2 aabaaf
为了从其他文章过来的同学更好的计算时间复杂度,这里用 n表示s1的长度,m表示s2的长度。
两层for循环,忽略时间复杂度O(n*m),肯定能走过每一种情况。
- for(int i=0;i<s1.size();i++) //这里s1是string类型,如果是char数组的话,用strlen(s1)表示长度一样的。
-
- {
-
- for(int j=0;j<s2.size();j++)
-
- {
-
- if(s1[i+j]!=s2[j]) //j是比较时s2字符串的下标,i+j是s1比较到的下标位置
-
- break;
-
- }
-
- }
如果暴力枚举的话,字符串长度很短的情况,是可以卡过去的,但是当长字符串到了1e6~1e7的长度左右,很容易被卡时间。
所以我们考虑,有没有什么办法来优化呢?我们可以发现,在比较两字符串是否相等的过程中,有很多重复无意义的比较。
比如上文给出的字符串s1和s2,我比较到i=0,j=6的字符串,能发现s1中的 aabaab前5位 与s2的 aabaaf前5位 相同,但是第6位 b!=f,所以i++,j=0 再次从头比较。
但是聪明的你们肯定也发现了,我们第一次比较,虽然没有找出子串,但是在s1中找出了两个aab,都与s2的前三位aab相等。
如果我们想找完全相等的字符串的话,第一个aab已经被否定,那我们为什么不直接从第二个aab再往后找呢?
这就是kmp算法相比正常暴力枚举所优化的地方。
为了实现刚才那种人能想到,但计算机想不到的思路,我们需要先对子串进行处理
大多数文章中用 next数组/pre数组 来表示子串的前缀表,这里我怕大家混淆其它算法中的next含义,我用kmp数组来表示
真前后缀概念:不用概念,一看就懂。注意一下真前后缀不包含自身。
即aabaaf的前缀子串有:a,aa,aab,aaba,aabaa 后缀子串有:f,af,aaf,baaf,abaaf
从i=0到i=5比较,求每个前缀子串的最长相等前后缀的长度
简单说明:比如aabaa串,最长的一段,是它的前缀aa和后缀aa相等,长度为2
a | 0 | 无前缀 无后缀 | 长度为0 | |
aa | 1 | 前缀a 后缀a | 长度为1 | a |
aab | 0 | 前缀aa 后缀ab | 长度为0 | |
aaba | 1 | 前缀aab,后缀aba | 长度为1 | a |
aabaa | 2 | 前缀aaba 后缀abaa | 长度为2 | aa |
aabaaf | 0 | 前缀aabaa 后缀abaaf | 长度为0 |
可得:前缀表(临时数组) 010120 //kmp数组
- //伪代码如下:
-
- int kmp[1000010]; //全局变量初值为0,如果有多组测试样例,记得每次初始化
-
- void getkmp(子串s)
-
- {
-
- memset(kmp,0,sizeof(kmp));
-
- 初始化 j=0;
-
- kmp[0]=0;
-
- for(i=1; i<s.size(); i++) //i继续匹配,for循环捋一遍子串
-
- {
-
- 前后缀不相同
-
- while(s[i]!=s[j] && j>0) //if会错,需要连续回退,0是边界
-
- j=kmp[j-1];
-
- 前后缀相同
-
- if(s[i]==s[j])
-
- kmp[i]=++j; //j继续匹配,并且kmp[i]赋值
-
- }
-
- }
学会如何得到kmp数组之后,那找到了前缀表有什么用呢?
kmp数组,我们能得到子串的最长相等前后缀,我们能根据它相等的部分,直接从相等部分的下一位开始比较。
比如 母串aabaabaaf,子串aabaaf
我们暴力枚举,当i=0,j=6比较到aabaab与aabaaf并不相等时,我们不用再让i=1,j=0重新比较。
我们重新定义i和j,让i指向母串s1的下标,j指向子串s2的下标。
头脑风暴,前方高能
我们这样移动i和j,i和j同时移动并对比,i=5并且j=5时,因为b!=f,所以我们将j回退,考虑我们如果能比较到f这个位置,那b前面(母串)一定存在aa和aabaaf(子串)中f前的aa重复,而aab!=aaf,则我们回退到上一个aa(子串)的位置。
因为之前计算的kmp数组,当子串为aabaa时,最长相等前后缀为2,即当下标为2时,前边的aa相当于比完了,所以j回退到2,i仍然是5,s2[j]与s1[i]再进行比较,我们就能直接比较出aab(母串)与aabaaf(子串)前面相等,于是i++,j++,继续向后比较,直到在母串中找出子串。
考虑一种情况,如果母串是aac,子串aabaaf,我们j回退到2,s1[i]=’c’ , s2[j]=’b’,还是不相等怎么办?那就和之前一样处理,找c前面的子串对应的前缀表,可以看到aa对应值为1,所以j回退到1,再次进行比较。如果j一直回退到0,还没有找到相等的部分,那就说明已经退到边界了,得重新比较了。
如果光看分析看不懂的话,建议结合代码,自己拿张纸画出子串和母串,然后手动模拟观察i和j的位置。
- //代码实现部分:
-
- for(int i=1,j=0;i<s2.size();i++) //得到kmp数组,s1长字符串(母串),s2短串(子串)
- {
- while(j && s2[i]!=s2[j])
- {
- j=kmp[j-1];
- }
-
- if(s2[i]==s2[j])
- {
- kmp[i]=++j;
- }
- }
- for(int i=0,j=0;i<s1.size();i++) //在s1串中找到第一个s2串
- {
- while(j && s1[i]!=s2[j])
- {
- j=kmp[j-1];
- }
-
- if(s1[i]==s2[j])
- {
- j++;
- }
-
- if(j==s2.size())
- {
- ans=i;
- break;
- }
- }
板子题:P3375 【模板】KMP字符串匹配 -> kmp模板题(洛谷)
AC代码如下:
- /*
- P3375 【模板】KMP字符串匹配
- https://www.luogu.com.cn/problem/P3375
- 思路:此题要求不止找到一个出现的子串,而是要找到所有出现过的子串,所以我们需要根据题意,修改查找部分的代码。
- */
- #include<bits/stdc++.h>
- using namespace std;
- const int N=1e6+5;
- typedef long long ll;
- string s1,s2;
- int kmp[N];
- int main()
- {
- cin>>s1>>s2;
- //printf("%d %d\n",s1.size(),s2.size());
- for(int i=1,j=0;i<s2.size();i++)
- {
- while(j && s2[i]!=s2[j])
- {
- j=kmp[j-1];
- }
- if(s2[i]==s2[j])
- {
- kmp[i]=++j;
- }
- }
- for(int i=0,j=0;i<s1.size();i++)
- {
- while(j && s1[i]!=s2[j])
- {
- j=kmp[j-1];
- }
- if(s1[i]==s2[j])
- {
- j++;
- }
- //if(i-j==-1)
- // printf("%d %d\n",i,j);
- if(j==s2.size())
- {
-
- printf("%d\n",i-j+2);
- j=kmp[j-1];
- }
- }
- for(int i=0;i<s2.size();i++)
- printf("%d ",kmp[i]);
- return 0;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。