2024-8-28 ·最后更新时间 2024-8-28
1
,
R
e
c
o
m
m
e
n
d
a
t
i
o
n
\Large\mathcal{1,Recommendation}
1,Recommendation
Knuth-Morris-Pratt 字符串查找算法,简称为KMP算法,常用于在一个文本串 S 内查找另一个文本 P 的出现位置,因为时间复杂度优异而被广泛使用。
这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。
2
,
P
r
e
f
i
x
f
u
n
c
t
i
o
n
\Large\mathcal{2,Prefix\ function}
2,Prefix function
在正式学习 KMP 算法之前我们要对前缀函数有一定的了解。
比如给你一个字符串:
S
=
A
B
A
D
A
B
A
S=ABADABA
S=ABADABA 。
那么前缀后缀相同时的最长长度是多少?很显然一定
3
3
3
A
B
A
\color{red}{ABA}
ABA
D
D
D
A
B
A
\color{red}{ABA}
ABA。
那么在数学中我们就会给这种形式的数值常用
π
\pi
π 来表示。
那么我们如果把所有
S
S
S 的前缀给列出来,并且对与每个前缀都求出对应的
π
\pi
π 那么就形成了前缀函数,如:
i i i | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
S S S | A A A | A B AB AB | A B A ABA ABA | A B A D ABAD ABAD | A B A D A ABADA ABADA | A B A D A B ABADAB ABADAB | A B A D A B A ABADABA ABADABA |
π \pi π | 0 0 0 | 0 0 0 | 1 1 1 | 0 0 0 | 1 1 1 | 2 2 2 | 3 3 3 |
这就是我们的前缀函数,但是…它和 KMP 有什么关系呢?
3
,
K
M
P
\Large\mathcal{3,KMP}
3,KMP
接下来我就要根据前缀函数来推演出 KMP 算法。
假设文本串
S
=
E
A
C
E
E
A
B
C
S=EACEEABC
S=EACEEABC,模式串
P
=
E
A
B
P=EAB
P=EAB 。
考虑什么时候
P
P
P 可以匹配上
S
S
S 的字串。
我们可以这样,先用一个奇妙字符给他们衔接起来就变成了
E
A
B
#
E
A
C
E
E
A
B
C
EAB\#EACEEABC
EAB#EACEEABC 。
然后我们就可以轻而易举地根据前缀函数得知,当且仅当
π
i
=
l
e
n
(
P
)
\pi_i = len(P)
πi=len(P) 的时候才可以匹配上。
我们可以浅浅证明一下,因为前缀函数的定义就是到了
i
i
i,
π
i
\pi_i
πi 为前缀后缀相同时的最长长度,因为有特殊符号所以
m
a
x
{
π
i
}
=
l
e
n
(
P
)
max\{\pi_i\} = len(P)
max{πi}=len(P) 所以
P
P
P 匹配上时,
π
i
=
l
e
n
(
P
)
\pi_i=len(P)
πi=len(P)。
接下来文中出现的
S
均为一般的字符串
接下来文中出现的 S 均为一般的字符串
接下来文中出现的S均为一般的字符串
那么接下来的问题就是如何求
π
i
\pi_i
πi 了。
我们可以把字符串想象成一些点,那么就变成了:
那么如果我们现在知道
π
i
−
1
\pi_{i-1}
πi−1 的数值的话:
那么轻而易举地我们可以知道当
S
π
i
−
1
+
1
S_{\pi_{i-1}+1}
Sπi−1+1 和
S
i
S_i
Si 相等时
π
i
=
π
i
−
1
+
1
\pi_i = \pi_{i-1}+1
πi=πi−1+1,于是我们可以写出一个不完整的代码:
for(int i=1;i<=s.size();++i){
int len=pi[i-1];
if(s[i]==s[len]){
pi[i]=len+1;
}
}
BUT 不相等怎么办?那我们是不是尽量考虑次小的 π i \pi_i πi?那我们是不是又可以写出一个代码:
for(int i=1;i<=s.size();++i){
int len=pi[i-1];
while(s[i]!=s[len]){
len=next_pi(i-1);
}
if(s[i]==s[len]){
pi[i]=len+1;
}
}
接下来我们就要解决 next_pi(x) 这个函数怎么求,我们可以再画一个图:
别问为什么图变了,如果我们仔细观察
π
i
−
1
′
\pi^{'}_{i-1}
πi−1′ 和
π
i
−
1
\pi_{i-1}
πi−1 的关系我们可以发现,
[
0
,
π
i
−
1
′
]
[0,\pi^{'}_{i-1}]
[0,πi−1′] 这段字符串本质上是
[
0
,
π
i
−
1
]
[0,\pi_{i-1}]
[0,πi−1] 的一段后缀,又根据前缀函数可知,
[
i
−
π
i
−
1
′
,
i
−
1
]
[i-\pi^{'}_{i-1},i-1]
[i−πi−1′,i−1] 一定是与
[
0
,
π
i
−
1
′
]
[0,\pi^{'}_{i-1}]
[0,πi−1′] 相等的,所以
[
0
,
π
i
−
1
′
]
[0,\pi^{'}_{i-1}]
[0,πi−1′] 是等于
[
0
,
π
i
−
1
]
[0,\pi_{i-1}]
[0,πi−1] 的后缀的!也就是
π
i
−
1
′
\pi^{'}_{i-1}
πi−1′ 是等同于
π
p
i
i
−
1
\pi_{pi_{i-1}}
πpii−1 的所以我们终于可以把代码补全了qwq:
for(int i=1;i<=s.size();++i){
int len=pi[i-1];
while(len&&s[i]!=s[len]){
len=pi[len-1];
}
if(s[i]==s[len]){
pi[i]=len+1;
}
}
那么,如果你完完整整的看完了这篇博客,你可能会觉得这和你印象中的 KMP 不太一样,但是如果你把到 # \# # 之前的和之后的单独拆开你会发现这就变成了你熟悉的 KMP,但这也表示着重要的一点,你需要点赞,收藏,关注我qwq。