算法笔记 KMP
算法笔记 KMP
-
什么是KMP算法
KMP算法是D.E.Knuth、J.H.Morris和V.R.Pratt几乎同时发现的一种字符串匹配算法。他要解决的是如下问题:
(模式串的匹配问题)在某个字符串S中,求字符串T出现的位置。其中字符串S叫做主串,字符串T叫做模式串。若字符串T在字符串S中出现,那么返回它第一次出现的位置。若没有出现,则返回-1。
例如:
主串S: ABCABCAB 模式串T: ABC
则模式串T在S中出现了2次,第一次出现的位置是0,即主串S中第一个A的位置。
-
如何解决
1.暴力法
我们很容易能想到一种解决方法:令i, j是两个分别指向主串S和模式串T的指针(即数组下标)。
第一步:令i,j初始化为0,即分别指向主串S和模式串T的开头。
第二步:
1°若S[i] == T[j],则将i和j分别自增1,为下一次比较做准备。
2°若S[i] != T[j],那么匹配失败(称为失配),i指向一开始时i的下一位,j需要重置为0。
第三步:循环第二步,若j == T.length(),说明找到了这个字符串,匹配成功,否则若i == S.length(),则说明找到主串S中的最后都没能匹配成功,即T在S中没有出现,返回-1。
算法如图所示:
据此算法,我们可以得出如下代码:
#include<bits/stdc++.h> using namespace std; int main(void){ string s, t; cin >> s >> t; int i = 0, j = 0; int len1 = s.length(), len2 = t.length(); while(i < len1 && j < len2){ if(s[i] == t[j]){ i++, j++; } else{ i = i - j + 1;//j是指向当前模式串失配位置的指针,也就是主串和模式串匹配字符的个数 //所以i-j表示将i退回至起点,+1即将i指向一开始i的下一位 j = 0;//重置j为0 } } if(j == len2){ cout << i - j + 1 << endl;//输出在主串中模式串第一次出现的位置,i-j的含义和上面相同 //这里我们将下标从1开始,所以+1 } //j没有到达模式串的最后,说明没有匹配成功 else{ cout << -1 << endl; } return 0; }
这个算法在最坏情况下,每次比较都是比到模式串的最后一个字符,发现不匹配。如:
主串:aaaaaaaaaaaaaaaaaaaaaaab 模式串:aaaaab
这时最坏情况下的时间复杂度是O(mn)(m表示主串的长度,n表示模式串的长度)。当在处理百万量级长度的字符串时,这显然不够好,所以我们对算法进行优化。KMP算法便应运而生。
2.KMP算法
在朴素的暴力法中,当字符串失配时,总是只将指针i自增1,其实这是对已有信息的极大浪费。
在字符串失配的时候,其实已经知道主串和模式串的前j个字符是匹配的。那么我们能不能用这一信息,移动指针j,而指针i可以保留在失配的位置,而不用回到上一次i的后一位?
还是回到刚才的字符串,
如果我们人脑操作,在失配的时候,因为前两个字符A,B是适配的,所以我们不可能再将i指向第二个B,将j归零。我们可以直接将j指向第一个A,而不动i。(如图)然后继续比较。
这个例子可能还不是很清楚,考虑如下例子:
观察②到③的过程,我们没有直接把j放到模式串的第0位,而是从第1位开始比较。因为在②失配的时候,我们已经知道②的前三个字符和处在i指针前面的三个字符是匹配的,而j指针前的字符串中,本身它的第一个字符和最后一个字符是相同的。那么我们可以得到:
因为,第②步中,j指针前的字符串是和主串i之前的相同长度的字符串匹配
又因为,j指针前的字符串的第一个字符和最后一个字符相同
所以,模式串S的第一个字符和主串指针i之前的最后一个字符相匹配。
这是KMP算法的关键。其中j指针前的字符串第一个字符和最后一个字符相同,可以扩展为j指针前的字符串的前n个字符(称为前缀)和后n个字符(称为后缀)相同,那么模式串S中j指针的前n个字符就和主串指针i前的n个字符相同。
表述起来可能有点令人费解,看图。
所以,我们只要知道指针j前的字符串最长的相同前缀后缀的长度。每次失配的时候,我们只需将j指针移到最长相同前缀后缀之后的位置继续比较,而i指针不动,这样能大大减少算法运行的时间。
同时,如果某次失配时,j指针已经指向了模式串的开头,那么这个时候j指针已经无法移动了(j指针前的字符串为空串,最长相同前缀后缀长度一定为0,j指针永远为0),这时我们就要将i指针向后移动一位。如图中④到⑤步。
我们将指针j前的字符串的最长相同前缀后缀长度记录在next[i]数组中。由此我们可以得出KMP算法的代码。
int kmp_search(string s, string p){
int i = 0;
int j = 0;
int len1 = s.length(), len2 = p.length();
while(i < len1 && j < len2){
if(j == -1 || s[i] == p[j]){//为什么会判断j是否等于-1?之后会讲
i++, j++;//如果匹配,指针自增1
}
else{
j = next[j];//不匹配了,j指针指向next[j]
}
}
if(j == len2){//模式串匹配完了,即在主串中找到了模式串
return i-j;//i-j是模式串第一次出现的位置
}
else
return -1;//没找到,返回-1
}
那么我们现在的问题就转化成了求next[]数组,即求指针j前的字符串的最长相同前缀后缀的长度。这也是KMP算法的核心部分,也是最难理解的部分。
求指针j前的字符串的最长相同前缀后缀的长度,相当于一个模式串的长度为j的前缀和另一个相同的模式串进行匹配的过程。其中长度为j的前缀比较充当的是后缀的作用(这里比较难理解)。因为后缀一定是连续的,所以当该前缀的长度变大后,如果最后一个字符是匹配的,那么加上前面的匹配串的长度,就是对应长度为j的前缀的的最长相同前缀后缀的长度。(由已知状态推出未知状态,有点像动态规划)
我们用一个i(这里为了方便,j和i互换了)指针指向该前缀,那么i指针前的字符串和另一个与模式串相同的串的最大匹配的串长,就是next[i]的长度。注意,这里next[i]数组的下标i表示长度为i的前缀的最长相同前缀后缀的长度,即在这里,字符串第一个数字的i是1,而不是0。而匹配的模式串,下表从0开始。
和之前一样,文字叙述比较难理解,看图。(红字部分表示现在的模式串的长度为i的前缀,阴影部分表示匹配的部分)
首先我们将next[0]设为-1。-1是一个特征值,表示现在j指针已经指向了模式串的开头,此时要将i指针向后移动一位,这就是上面kmp_search()函数第5行判断j是否等于-1的原因。
由于单个字符的前缀是空串,所以next[1] = 0,我们从i=2开始处理
注意失配的时候,情况和匹配主串和模式串的方法相同。若配对成功,则i,j指针均自增1。若出现了失配,那么i指针不变,将j指针变为next[j]的数值,因为i从2开始,而j从1开始,并且i是单调增加的,且j增加的时候,i必定增加,所以必有j < i,即当失配时,所需求得得next[j]一定已经算好了(有点类似动态规划的思想)。这时我们将j置为next[j]。和主串和模式串匹配相同地,若j在模式串的开头,并且此时失配了,可以得到此时的j = -1,说明要将i自增1。
由此我们可以得到代码:
const int N = 1e6+7;
int next[N];
void getNext(string p){
next[0] = -1;
int i = 0, j = -1;
int len = p.length();
while(i < len){
if(j == -1 || p[i] == p[j]){
++i, ++j;
next[i] = j;
}
else j = next[j];
}
}
将getNext()函数和kmp_search()函数结合起来,就可以得到KMP的完整算法。
- 例题
洛谷P3375 【模板】KMP字符串匹配
-
题目描述
如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。
为了减少骗分的情况,接下来还要输出子串的前缀数组next。
-
输入输出格式
- 输入格式
第一行为一个字符串,即为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<=1000000
-
Tips:
这题和标准KMP的不同之处在于要输出所有的匹配,而不是第一个匹配的位置,多用几次KMP循环即可。
-
Answer:
#include<bits/stdc++.h> using namespace std; const int N = 1e6+7; int next[N]; void getNext(string p){ next[0] = -1; int i = 0, j = -1; int len = p.length(); while(i < len){ if(j == -1 || p[i] == p[j]){ ++i, ++j; next[i] = j; } else j = next[j]; } } int kmp_search(string s, string p, int i){ int j = 0; int len1 = s.length(), len2 = p.length(); while(i < len1 && j < len2){ if(j == -1 || s[i] == p[j]){ i++, j++; } else{ j = next[j]; } } if(j == len2){ return i-j; } else return -1; } int main(void){ string s, p; cin >> s >> p; getNext(p); int i = 0; while(true){ i = kmp_search(s, p, i); if(i == -1){ break; } else{ cout << i + 1 << endl; i++; } } for(unsigned int k = 1; k <= p.length(); k++){ cout << next[k] << ' '; } cout << endl; return 0; }