KMP字符串查找
问题如下:
给定一个字符串 S S S,以及一个模式串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。模式串 P P P在字符串 S S S中多次作为子串出现。求出模式串 P P P在字符串 S S S中所有出现的位置的起始下标。
这个问题的 b r u t a l − f o r c e brutal-force brutal−force解法是显然的,只要一次次将模式串与原串比对,比对完成则返回第一个下标,比对失败则将模式串向右移动一位,继续进行比对,直至模式串右端达到原串的结尾。
问题是这个算法的效率巨特么低。若S的长度为M,P的长度为N,则时间复杂度是 O ( M N ) O(MN) O(MN)。在M为 1 0 5 10^5 105,N为 1 0 6 10^6 106的情况下,运算次数就会直接爆炸( 1 0 11 10^{11} 1011)。
考虑对 b r u t a l − f o r c e brutal-force brutal−force解法进行优化。这个解法的时间复杂度取决于两方面,一方面是比对操作,一方面是移动操作。原解法的比对操作每次都是从模式串的开头开始比对,且移动操作每次都只移动一位,这是两个使时间复杂度爆炸的重要因素。
每一次比对失败并不是一开始就失败的,若是能将已经比对成功的部分利用起来,想必能够减少时间复杂度。
综上,我们应该去寻找一种能够继承上一次比对成果的算法。
优化
关于下标的问题
我们考虑在匹配过程中对S串使用从0开始的下标,在录入P串时对P串使用从1开始的下标, n e ne ne数组表示相等的前后缀真子串的最大长度。具体原因之后再说。
具体优化过程
基本思路
考虑如图的情况。
现在有 i i i和 j j j两个指针,现寻找一种算法,使 i i i指针的移动是单调的,通过移动 j j j指针实现对上一次匹配结果的继承。
为了结果的简便,我们对比 P [ j + 1 ] P[j+1] P[j+1]和 S [ i ] S[i] S[i]。
若当模式串P上的指针指到 j j j,S串指到i时,比对失败,则直到 j j j都已经比对成功。我们有 S [ i − j + 1.. i ] = P [ 1.. j ] S[i-j+1..i]=P[1..j] S[i−j+1..i]=P[1..j]。此时我们要移动P串。但是,我们不需要只移动1位——我们要移动更多的位数,这个位数将是由已知信息推出的第一个可能使匹配成功的位数,移动的位数比这个数字小将必定匹配失败,比这个数字大将可能漏解。
下面证明我们应将
j
j
j移动至
n
e
[
j
]
ne[j]
ne[j],其中
n
e
(
i
)
ne(i)
ne(i)满足:
n
e
(
i
)
=
m
a
x
(
{
l
∣
P
[
1..
l
]
=
P
(
i
+
1
−
l
.
.
i
)
}
)
ne(i) = max(\{l\;|\, P[1..l] = P(i +1- l..i)\})
ne(i)=max({l∣P[1..l]=P(i+1−l..i)})
注意:此时P的下标从1开始!!
即,我们考察 P [ 1.. i ] P[1..i] P[1..i]中所有内容相同的的前缀字符串和后缀字符串,这些字符串中的最大长度即为 r e ( i ) re(i) re(i)。
若移动的长度小于这个数字,如图,移动浅蓝色的距离。
若留在已匹配部分的P串能够匹配成功,则此时浅蓝色部分显然满足 P [ 1.. l ] = P ( i + 1 − l . . i ) P[1..l] = P(i + 1 - l..i) P[1..l]=P(i+1−l..i),这个长度显然大于 n e ( i ) ne(i) ne(i),与定义矛盾!故而必然匹配失败。移动这个位数将可能匹配成功(移动后的P串留在已匹配部分的内容全部比对成功,之后的内容还未经过比对),故比这个数字大的移动位数将可能漏解。
循环至可能匹配成功
之后若匹配依旧不成功( P [ j + 1 ] ≠ S [ i ] P[j+1] \ne S[i] P[j+1]=S[i]),则我们将继续移动j指针。最坏情况是将j指针移动至0,重新开始匹配。
和原串匹配
有两个循环的条件,一是
P
[
j
+
1
]
≠
S
[
i
]
P[j+1] \ne S[i]
P[j+1]=S[i],二是
j
>
0
j > 0
j>0。跳出循环时,至少有一个为假,若第一个为假,则完成循环,j向右移动一位;若第二个为假,则有
j
=
0
j = 0
j=0,此时需进行判断p[j + 1] == s[i]
,若为真则j向右移动一位,若为假则只能移动i了。
在以上步骤进行完毕后,对j进行边界检测,若 j = n j = n j=n,则输出结果。
代码如下:
for (int i = 0, j = 0; i < m; i++){
while (j > 0 && p[j + 1] != s[i]) j = ne[j];
if (j < n && p[j + 1] == s[i]){
j++;
}
if (j == n){
printf("%d ", i - (n- 1));
j = ne[j];
}
}
求解 n e ( i ) ne(i) ne(i)
在求解 n e ( i ) ne(i) ne(i)的过程中想到使用动态规划是自然的(或者说要使用最基本的信息推所有的信息)。考虑求 n e ( i ) ne(i) ne(i),若 P [ i ] = P [ n e ( i − 1 ) + 1 ] P[i] = P[ne(i - 1) + 1] P[i]=P[ne(i−1)+1],则 n e ( i ) = n e ( i − 1 ) + 1 ne(i) = ne( i - 1) + 1 ne(i)=ne(i−1)+1。若不满足条件,则必然有 n e ( i ) − 1 < n e ( i − 1 ) ne(i) - 1 < ne(i - 1) ne(i)−1<ne(i−1)。下面来证明这一点。
显然 n e ( i ) − 1 ≠ n e ( i − 1 ) ne(i) - 1 \ne ne(i - 1) ne(i)−1=ne(i−1),若 n e ( i ) − 1 > n e ( i − 1 ) ne(i) - 1 > ne(i - 1) ne(i)−1>ne(i−1),则由 n e ne ne数组的定义, n e ( i − 1 ) = n e ( i ) − 1 ne(i - 1) = ne(i) - 1 ne(i−1)=ne(i)−1,矛盾!,故而 n e ( i ) − 1 < n e ( i − 1 ) ne(i)-1<ne(i-1) ne(i)−1<ne(i−1)。
接下来寻找不满足条件之后的解决方法。我们要在 n e ( i − 1 ) ne(i - 1) ne(i−1)范围内找最大的前后缀相等的字符串,则根据 r e re re数组的定义,这个字符串的长度为 n e ( n e ( i − 1 ) ) ne(ne(i - 1)) ne(ne(i−1))。若此时依旧没有 P [ i ] ≠ P [ n e ( n e ( i − 1 ) ) + 1 ] P[i] \ne P[ne(ne(i-1))+1] P[i]=P[ne(ne(i−1))+1],则继续这个过程(在 n e ( n e ( i − 1 ) ) ne(ne(i -1)) ne(ne(i−1))里找。最终结束循环的条件应有两个:一个是满足 P [ i ] = P [ n e ( n e ( i − 1 ) ) + 1 ] P[i] = P[ne(ne(i-1))+1] P[i]=P[ne(ne(i−1))+1],另一个是迭代之后从 n e ne ne数组取出的值为0。如果没有第二个条件,则若一直无法满足第一个条件( P [ i ] ≠ P [ 0 + 1 ] P[i] \ne P[0 + 1] P[i]=P[0+1],这是很有可能的),我们将进入死循环(取出的值依旧为0).
跳出循环后,我们只知道两个条件中至少有一个为假。故而我们再进行一次判断(p[i] == p[end + 1]
)(其中end是最后从
n
e
ne
ne数组中取出的值),若这个判断为真,则我们有
n
e
[
i
]
=
e
n
d
+
1
ne[i] = end + 1
ne[i]=end+1,若为假,则必有
P
[
i
]
≠
P
[
e
n
d
+
1
]
∧
e
n
d
=
0
P[i] \ne P[end + 1] \wedge end =0
P[i]=P[end+1]∧end=0,故而有
n
e
[
i
]
=
0
ne[i] = 0
ne[i]=0。
代码如下:
for (int i = 2; i <= n; i++){
int end = ne[i - 1];
while (p[i] != p[end + 1] && end > 0){
end = ne[end];
}
if (p[i] == p[end + 1]) ne[i] = end + 1;
else ne[i] = 0;
}