今天我们来讲Kmp算法,这个算法我个人认为真的很巧妙,具体的题目是这样的。
第一步,整理清楚题意,就是有一个长字符串S,我们要从字符串S中找出小字符串P的位置,输出出来。
第二步,用简单暴力的方法做出来,就是用双重循环,以接近O(m*n)的时间复杂度做出。
具体代码如下:
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
char s[M], p[N];
int n, m;
int main(){
cin >> n >> p >> m >> s;
for(int i = 0; i < m; i ++ ){
bool st = true;
int k = i;
for(int j = 0; j < n; j ++ ){
if(s[k] != p[j]){
st = false;
break;
}
k ++;
}
if(st == true){
printf("%d ",i);
}
}
return 0;
}
其实就是用暴力枚举的方法,在s中每一个位置都对字符串p进行匹配,如果匹配到了就进行输出。如果不那么追求效率,或者不要求百分百全过,这也是个不错的省力好办法。
第三步,对之前的算法进行优化,我们这道题建议用Kmp,简单来说Kmp的思想就是省略相同折叠字符串的检索时间。
代码如下
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 1000010;
char p[N], s[M];
int ne[N];
int main(){
int n,m;
cin >> n >> p + 1 >> m >> s + 1;
for(int i = 2, j = 0; i <= n; i ++ ){
while(j && p[j + 1] != p[i]) j = ne[j];
if(p[j + 1] == p[i]) j ++;
ne[i] = j;
}
for(int i = 1, j = 0; i <= m; i ++){
while(j && p[j + 1] != s[i]) j = ne[j];
if(p[j + 1] == s[i]) j ++;
if(j == n){
printf("%d ",i - n);
j = ne[j];
}
}
return 0;
}
如图
Kmp的核心是next数组,通俗来讲,它是一个指针数组,指向上一个重复的部分。
举个例子:当检索到c的时候匹配失败,p字符串会退回到之前重复的部分匹配,而不是从头开始匹配。比如上面图中的P字符串,ababa,下标分别是12345,下标从1开始,第一个a的重复部分显然没有,所以next[1] = 0,之后的b显然也没有重复的部分 next[2] = 0,第二个a可以找到第一个a作为重复的部分 next[3] = 1,第二个b可以找到ab作为重复的部分,next[4] = 2,之后next[5] = 3。这就是next数组的原理。
我们的p的下标是j,s的下标是i。但在检索的时候我们常常习惯用j+1和i相互匹配。如图,p[5]与s[5] 不匹配,即a和c不匹配,也就是p[j + 1] 和 s [i] 不匹配,这时的 j 是4。所以,要退回到p[1],p[j + 1]和 s [5] 也不匹配,所以,退到p[0],p[0]没有字符,这时就跳过,从s[6]与p[1]开始匹配,之后一一匹配,直到匹配成功。
现在来看代码,第一个for循环是来寻找p数组中的ne数组,也就是最长的重复循环。while(j && p[j + 1] != p[i]) j = ne[j]; 就是说在 j 不等于0,并且在p[j + 1]与p[ i ]不匹配时,就将 j 变成ne[ j ],也就是跳回上一个重复的位置(意思就是就向前找可以匹配的位置,直到找到,或者将 j 变成0,也就是回到初始的位置)。if(p[j + 1] == p[i]) j ++;如果匹配成功的话,就让 j 去到下一个位置。ne[i] = j; 将 j 变成 i 的循环位置(由于之前的两步已经保证了当 j + 1满足条件,就跳转到 j + 1的位置,如果 j + 1不满足的话,就向前找,直到找到,或者将 j 变成0,也就是回到初始的位置)
接下来看第二个循环,这个循环和之前的循环如出一辙,但作用却不太相同,这个循环是用来匹配 p串和 s 串的。 while(j && p[j + 1] != s[i]) j = ne[j]; 进行 j = ne[ j ]操作,直到 j 不为0,或者匹配失败。if(p[j + 1] == s[i]) j ++; 如果匹配成功,那么就将 j 移动到下一个位置。
if(j == n){
printf("%d ",i - n);
j = ne[j];
}如果匹配成功,就输出 i - n的位置(由于下标是从1开始的)。
以上就是Kmp的详解,刚开始看Kmp的时候可能会有一点抽象,但如果深究下来,其实也不是很难,还是熟能生巧的事。
刚开始第一次做还是建议将代码理解个大概,将每一句代码的含义弄明白,之后背下来,到可以轻松默写的程度,保证可以应用就ok了,至于原理不用太深究。