最近学习了一下 KMP 算法,写一下总结,免得忘了。
KMP算法:
1.1 暴力匹配的浪费:
假设正在进行下图这样的匹配,
暴力匹配一旦匹配失败,模式串就会回退到开头进行匹配;
我们可以看到,失配字符 ‘C’ 的前两个字符 (AB) 和开头 (AB) 是一样的,如果挪到开头去匹配,很明显会进行一些不必要的匹配;
显然,将箭头所指字符与失配处进行匹配才是 比较 优的。
KMP 算法就是基于这个思想的。只要知道有前缀与后缀匹配 的 最大长度,那某个位置失配之后,就能迅速的像上面那样跳到一个 比较 优的位置进行匹配。
为什么是 “比较优” ?后面再说。
1.2 什么叫 “前缀和后缀匹配” ?
对于一个位置
i
i
,如果 ,那么就说
i
i
位置前缀匹配后缀的最大长度为 ;我们用
Next
N
e
x
t
数组存起来,即
Next[i]=k
N
e
x
t
[
i
]
=
k
;
那么,对
Next[]
N
e
x
t
[
]
下个定义:
∙Next[i]:str[0,i−1] ∙ N e x t [ i ] : s t r [ 0 , i − 1 ] 中前缀匹配后缀的最大长度。
如果我们计算出了模式串
P
P
的每个 ,那么在
pos
p
o
s
位置处失配时,立马就可以跳到
Next[pos]
N
e
x
t
[
p
o
s
]
处进行匹配;
所以,
Next
N
e
x
t
数组还可以这样理解:
∙Next[i]: ∙ N e x t [ i ] : 当 i i 位置失配时应该跳往的匹配位置。
比如上面的模式串 P P :
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
P | A | B | D | C | A | B | C | ‘\0’ |
Next | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
,对于模式串的首字符,我们统一为
Next[0]=−1
N
e
x
t
[
0
]
=
−
1
;
i=1
i
=
1
,前面的字符串为
A
A
,其最长相同真前后缀长度为 ,即
Next[1]=0
N
e
x
t
[
1
]
=
0
;
i=2
i
=
2
,前面的字符串为
AB
A
B
,其最长相同真前后缀长度为
0
0
,即 ;
i=3
i
=
3
,前面的字符串为
ABD
A
B
D
,其最长相同真前后缀长度为
0
0
,即 ;
i=4
i
=
4
,前面的字符串为
ABDC
A
B
D
C
,其最长相同真前后缀长度为
0
0
,即 ;
i=5
i
=
5
,前面的字符串为
ABDCA
A
B
D
C
A
,其最长相同真前后缀为
A
A
,即 ;
i=6
i
=
6
,前面的字符串为
ABDCAB
A
B
D
C
A
B
,其最长相同真前后缀为
AB
A
B
,即
Next[6]=2
N
e
x
t
[
6
]
=
2
;
i=7
i
=
7
,前面的字符串为
ABDCABC
A
B
D
C
A
B
C
,其最长相同真前后缀长度为
0
0
,即 。
还是开始那个例子,当 i=6 i = 6 时,失配,立马就可以跳到 i=2 i = 2 的位置进行匹配; faster了有没有?
1.3 如何求Next数组?
假设我们现在正在计算
Next[i]
N
e
x
t
[
i
]
,即之前的
Next[0,…,i−1]
N
e
x
t
[
0
,
…
,
i
−
1
]
已经得到了;
先上代码:
void Get_Next(string P) {
memset(Next, 0, sizeof(Next));
int P_len = P.length();
int i = 0; //当前枚举的位置
int p = -1; //上一次前后缀匹配的最大长度+1,说白了就是Next[i-1]的后一位
Next[0] = -1;
while(i < P_len) {
if(p == -1 || P[i] == P[p]) {
i++;
p++;
Next[i] = p;
}
else p = Next[p];
}
}
代码中最重要的就是那个 if…else… 语句;
一脸懵逼???不怕,上图:
假设
i
i
和 的位置如上图,上一次前后缀匹配的最大长度为
p−1
p
−
1
,即绿色的两个椭圆是相等的;
(1) if (P[i] == P[p]) ,则往后走就是了;
p == -1 又是为什么呢?一是 p 初始化为-1,总得往后走吧!二是当前后缀匹配长度为 0 时,p会为-1,这时候也得往后走;
(2) else
由
Next[p]
N
e
x
t
[
p
]
的定义可知,前两个绿色的椭圆是相等的;于是可知第
1
1
个和第 个椭圆是相等的;
因此,令
p=Next[p]
p
=
N
e
x
t
[
p
]
来加速匹配;
1.4 完整代码:
这其实是一道题:HDU-2087
/**********************************************
*Author* :XzzF
*Created Time* : 2018/4/26 7:07:32
*Ended Time* : 2018/4/26 12:56:37
*********************************************/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <algorithm>
#include <string>
#include <iostream>
using namespace std;
typedef long long LL;
const int inf = 1 << 30;
const LL INF = 1LL << 60;
const int MaxN = 10005;
string S, P;
int Next[MaxN + 5];
void Get_Next(string P) {
memset(Next, 0, sizeof(Next));
int P_len = P.length();
int i = 0; //当前枚举的位置
int p = -1; //上一次前后缀匹配的最大长度+1,说白了就是Next[i-1]的后一位
Next[0] = -1;
while(i < P_len) {
if(p == -1 || P[i] == P[p]) {
i++;
p++;
Next[i] = p;
}
else p = Next[p];
}
}
int KMP(string S, string P) {
Get_Next(P);
int i = 0; //index of S
int j = 0; //index of P
int cnt = 0;
while(i < S.length() ) {
if(j == -1 || S[i] == P[j]) {
i++;
j++;
}
else j = Next[j];
if(j == P.length()) {
j = 0; //不相交匹配则为0,相交匹配则为Next[j]
cnt++;
}
}
return cnt;
}
int main()
{
while(cin >> S >> P) {
if(S[0] == '#') break;
printf("%d\n", KMP(S, P));
}
return 0;
}
KMP优化:
上面还有一个问题没有解决,为什么说 “比较优” ?
2.1 问题所在:
其实一开始那个表格就能看到问题了,但还是看个”严重”点的吧!
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
P | A | B | A | C | D | A | B | A | C | E | ‘\0’ |
Next | -1 | 0 | 0 | 1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
假设
i=8
i
=
8
时失配,按照之前的KMP算法,就会把
p=3
p
=
3
处的字符拿过来匹配;然而,这两个字符是相同的,就增加了一些不必要的匹配;
稍微修改一下代码就能解决这个问题:
void Get_Nextval(string P) { //KMP优化
int P_len = P.size();
int i = 0; // P 的下标
int p = -1;
Nextval[0] = -1;
while (i < P_len)
{
if (p == -1 || P[i] == P[p])
{
i++;
p++;
if (P[i] != P[p])
Nextval[i] = p;
else
Nextval[i] = Nextval[p]; //既然相同就继续往前找
}
else
p = Nextval[p];
}
}
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
P | A | B | A | C | D | A | B | A | C | E | ‘\0’ |
Next | -1 | 0 | 0 | 1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
Nextval | -1 | 0 | -1 | 1 | 0 | -1 | 0 | -1 | 1 | 4 | 0 |
优化之后,当 i=8 i = 8 失配时,就能直接跳到 p=1 p = 1 处匹配,faster 了有没有?匹配方式还是没变的;
严格来讲,未优化的 KMP 算法叫 MP算法;但一般情况下,未优化的 KMP 算法已经够用了;
KMP算法(未优化):
Next[]
N
e
x
t
[
]
表示前后缀匹配的最大长度,还有一些骚操作,后面再说;
KMP算法(优化):
Nextval[]
N
e
x
t
v
a
l
[
]
表示最优长度,但不一定是最长;快是快了,功能少了;
复杂度:
O(n+m)
O
(
n
+
m
)
(并不会证明)
More about KMP :
KMP求最小循环节:
Picture one:
Picture two:
Picture three: