定义 p p p 串和 s s s 串,下标均从 1 1 1 开始。
p p p 的长度为 n n n, s s s 的长度为 m m m 。
求 p p p 在 s s s 中出现的所有的起始下标。
这里的 KMP 讲解仅为了方便后续快速回忆。
n e [ i ] ne[i] ne[i] 是对于 p p p 串来说的,令 k = n e [ i ] k=ne[i] k=ne[i]
n e [ i ] ne[i] ne[i] 的解释为:是指 p [ 1 , k ] = p [ i − k + 1 , i ] p[1,k]=p[i-k+1,i] p[1,k]=p[i−k+1,i] ,其中 p [ i , j ] p[i,j] p[i,j] 表示 p [ i ] , p [ i + 1 ] , ⋯ , p [ j ] p[i],p[i+1],\cdots,p[j] p[i],p[i+1],⋯,p[j] 这些字符构成的 p p p 的子串,满足 i ≤ j i\leq j i≤j 。
即对于 n e [ i ] ne[i] ne[i] ,表示 p [ 1 , i ] p[1,i] p[1,i] 这个子串的前 k k k 个字符构成的前缀和后 k k k 个字符构成的后缀是相同的。
我们考虑正常匹配时:
假设已经匹配到了 s [ i ] s[i] s[i] 和 p [ j ] p[j] p[j],此时已经匹配成功了 j − 1 j-1 j−1 个字符。
如果 s [ i ] ≠ p [ j ] s[i]\neq p[j] s[i]=p[j] ,暴力匹配的做法是需要从 s [ i − j + 1 ] s[i-j+1] s[i−j+1] 和 p [ 1 ] p[1] p[1] 重新匹配。
我们不想重新匹配,因为这其中可能有一些可以利用的信息。
考虑对于 k ≥ i − j + 1 k\geq i-j+1 k≥i−j+1 的下标,如果可以快速判断 p [ k , k + n − 1 ] ≠ s p[k,k+n-1]\neq s p[k,k+n−1]=s,则这些 k k k 可以跳过。
因为我们当前已经枚举到了 s [ i ] s[i] s[i] ,如果 p p p 长度为 l e n 1 len_1 len1 的前缀可以和 s [ i − l e n 1 , i − 1 ] s[i-len_1,i-1] s[i−len1,i−1] 匹配
那么我们就可以继续判断 p [ l e n 1 + 1 ] p[len_1+1] p[len1+1] 和 s [ i ] s[i] s[i] 是否匹配了。如此就可以进行下去匹配。
所以就需要找到最长的前缀长度 l e n 1 len_1 len1 ,而 n e [ i ] ne[i] ne[i] 的定义恰好就是对于 p [ 1 , i ] p[1,i] p[1,i] 这个字符串,最长的 l e n 1 len_1 len1 ,满足 p [ 1 , l e n 1 ] = p [ i − l e n 1 + 1 , i ] p[1,len_1]=p[i-len_1+1,i] p[1,len1]=p[i−len1+1,i] 。
如果 p [ l e n 1 + 1 ] p[len_1+1] p[len1+1] 和 s [ i ] s[i] s[i] 匹配,那皆大欢喜。
如果 p [ l e n 1 + 1 ] p[len_1+1] p[len1+1] 和 s [ i ] s[i] s[i] 不匹配,则需要找到一个尽可能大的,但是小于 l e n 1 len_1 len1 的 l e n 2 len_2 len2 。
这个 l e n 2 len_2 len2 得满足 p [ 1 , l e n 2 ] = p [ i − l e n 2 + 1 , i ] p[1,len_2]=p[i-len_2+1,i] p[1,len2]=p[i−len2+1,i] ,而这个更小的 l e n 2 len_2 len2,由于 KMP 的定义,则为 n e [ l e n 1 ] ne[len_1] ne[len1] 。
因为 p [ 1 , l e n 1 ] = p [ i − l e n 1 + 1 , i ] p[1,len_1]=p[i-len_1+1,i] p[1,len1]=p[i−len1+1,i]
如果有 p [ 1 , l e n 2 ] = p [ i − l e n 2 + 1 , i ] p[1,len_2]=p[i-len_2+1,i] p[1,len2]=p[i−len2+1,i] ,其中 l e n 2 < l e n 1 len_2len2<len1 ,且 l e n 2 len_2 len2 尽可能大
本质上是在 p [ i − l e n 1 + 1 , i ] p[i-len_1+1,i] p[i−len1+1,i] 中选择一个长度为 l e n 2 len_2 len2 的相等的前缀和后缀,也就是在 p [ 1 , l e n 1 ] p[1,len_1] p[1,len1] 中选择一个长度为 l e n 2 len_2 len2 的相等的前缀和后缀,也就是 l e n 2 = n e [ l e n 1 ] len_2=ne[len_1] len2=ne[len1]
void KMP(string p, string s) {
int n = p.size();
int m = s.size();
// 首空格是为了下标从 1 开始,p 的尾空格是为了防止越界,下面标明了可能越界的地方
// 加入为空格是用于避免越界的方法 2
p = " " + p + " ";
s = " " + s;
vector<int> ne(n + 1, 0);
for (int i = 2, j = 0; i <= n; ++i) {
while (j > 0 && p[j + 1] != p[i]) j = ne[j];
if (p[j + 1] == p[i]) j += 1;
ne[i] = j;
}
/*
这个循环是指当前已经匹配成功了 s[i-1] ,这是怎么匹配成功的呢
是指当前已经匹配成功的长度为 len1
那么 s[i-len1,i-1] 和 p[1,len1] 匹配成功
然后我们需要判断 p[len1+1] 与 s[i] 是否匹配
如果不匹配,则找到小于 len1 的最大的 len2,依旧需要满足 s[i-len2,i-1]=p[1,len2]
最后找到的为 j
1. 如果 p[j+1] != s[i] ,说明可以直接跳过 s[i] 这个字符了
2. 如果 p[j+1] == s[i] ,说明匹配成功,
p[j+1] == s[i] ,继续判断 p[j+2] 和 s[i+1] 即可
继续我们的操作,直到 j==n ,说明找到了一个,此时对应 s 中的下标是 i-j+1
*/
for (int i = 1, j = 0; i <= m; ++i) {
/*
这里当 j=n 时,j+1 可能就越界了,所以两种解决办法
1. 在下面 j==n 的判断中,加上 j = ne[j],因为 ne[j] < n
2. 在上面的 p 中,加入一个不属于 p 和 s 字符集中的字符,如空格space
*/
while (j > 0 && p[j + 1] != s[i]) j = ne[j];
if (p[j + 1] == s[i]) j += 1;
if (j == n) {
// p[1, n] 在 s 中出现的所有下标,从 1 开始计算 s 中的下标
cout << i - j + 1 << " ";
// j = ne[j]; 用于避免越界的方法 1
}
}
}