暴力匹配
S1:aabaabsaabaabaabsaabt
与 S2:aabaabsaabt
暴力匹配流程如下。
aabaabsaabaabaabsaabt
^ 从头开始匹配
aabaabsaabt
^
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 回退到上次起始位置的下一位置
aabaabsaabt
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 回退到上次起始位置的下一位置
aabaabsaabt
^ 从头开始,并且发生了不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 回退到上次起始位置的下一位置
aabaabsaabt
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 回退到上次起始位置的下一位置
aabaabsaabt
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 回退到上次起始位置的下一位置
aabaabsaabt
^ 从头开始,并且发生了不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 回退到上次起始位置的下一位置
aabaabsaabt
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ S2 越界,完全匹配成功
S1 与 S2 如果在第 i
位置没匹配上。则 S1 要回退到第 i+1
位置,而 S2 要回退到 0
位置,再重新开始匹配。
问题在于中间有大量计算被浪费,每次回退后的匹配宛如重新开始匹配一样,与上次匹配毫无关联。
前后缀
前缀:串中第一个字符开始,从前往后,最多到整个串倒数第二个字符,所构成的子串。
后缀:串中最后一个字符开始,从后往前,最多到整个串第二个字符,所构成的子串。
也就是前后缀是原来串的真子集,前缀不能包含尾,后缀不能包含头。
abcabcd
^
计算 d 之前的串中最长公共前后缀长度。
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
前缀 | a | ab | abc | abca | abcab |
后缀 | c | bc | abc | cabc | bcabc |
长度 | x | x | 3 | x | x |
所以最长公共前后缀长度为 3。
next 数组
next 数组每个元素,就是对应原串位置之前的最长公共前后缀长度。
以 aabaabsaabt
为例,计算 next 数组。
aabaabsaabt
^
之前为空串,规定为 -1
------------------------------------------------------------------------
aabaabsaabt
^
"a",前后缀是真子集,所以为 0
------------------------------------------------------------------------
aabaabsaabt
^
"aa"
前缀:a
后缀:a
最长:1
------------------------------------------------------------------------
aabaabsaabt
^
"aab"
前缀:a aa
后缀:b ab
最长:0
------------------------------------------------------------------------
aabaabsaabt
^
"aaba"
前缀:a aa aab
后缀:a ba aba
最长:1
------------------------------------------------------------------------
aabaabsaabt
^
"aabaa"
前缀:a aa aab aaba
后缀:a aa baa abaa
最长:2
------------------------------------------------------------------------
aabaabsaabt
^
"aabaab"
前缀:a aa aab aaba aabaa
后缀:b ab aab baab abaab
最长:3
------------------------------------------------------------------------
aabaabsaabt
^
"aabaabs"
前缀:a aa aab aaba aabaa aabaab
后缀:s bs abs aabs baabs abaabs
最长:0
------------------------------------------------------------------------
aabaabsaabt
^
"aabaabsa"
前缀:a aa aab aaba aabaa aabaab aabaabs
后缀:a sa bsa absa aabsa baabsa abaabsa
最长:1
------------------------------------------------------------------------
aabaabsaabt
^
"aabaabsaa"
前缀:a aa aab aaba aabaa aabaab aabaabs aabaabsa
后缀:a aa saa bsaa absaa aabsaa baabsaa abaabsaa
最长:2
------------------------------------------------------------------------
aabaabsaabt
^
"aabaabsaab"
前缀:a aa aab aaba aabaa aabaab aabaabs aabaabsa aabaabsaa
后缀:b ab aab saab bsaab absaab aabsaab baabsaab abaabsaab
最长:3
从而得到 next 数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
原串字符 | a | a | b | a | a | b | s | a | a | b | t |
最长公共前后缀长度 | -1 | 0 | 1 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 |
主串和模式串元素下标从 0 开始,就是上述结果。
如果下标是从 1 开始,则需要为整个 next 数组所有元素 + 1。
加速
利用 next 数组避免无意义的回退,从而加速整个匹配流程。
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ 发生不匹配,"aabaabsaab",最长公共前后缀为 3
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^ 无需回退
aabaabsaabt
^ 从前缀末尾的下一个元素开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ S2 越界,完全匹配成功
在第 i
位置发生不匹配,隐含两个信息:
- 在
0 ~ i-1
是完全匹配。 - 只有在第
i
位置不匹配。
aabaabsaabaabaabsaabt
^
aabaabsaabt
^ 在此处发生不匹配时,意味着前面位置都是匹配的,也就是说除该位置之前的串是一样的
所以在 i
之前的所有元素都无需再次匹配。只需要从 i
位置开始匹配。
又因为 S1 与 S2 之前匹配的 aabaabsaab
是一样的,其中前后缀 aab
是一样的,所以 S2 中的前缀可以跳过。
aabaabsaabaabaabsaabt
^
aabaabsaabt
^
假设如果此处还没有匹配上。
aabaabsaabxaabaabsaabt
^
aabaabsaabt
^ 发生不匹配,"aab",最长公共前后缀为 0
------------------------------------------------------------------------
aabaabsaabxaabaabsaabt
^ 无需回退
aabaabsaabt
^ 从前缀末尾的下一个元素开始。
此时还是不匹配,之前为空串,所以换个开头再次匹配。
------------------------------------------------------------------------
aabaabsaabxaabaabsaabt
^ 换个开头再次匹配
aabaabsaabt
^
------------------------------------------------------------------------
aabaabsaabxaabaabsaabt
^
aabaabsaabt
^ S2 越界,完全匹配成功
所以相当于没匹配上,根据 next 数组的值选择 S2 的起始位置,如果是 -1 那么久 S1 就换下一个元素开始匹配。
代码
// 原始暴力做法,O(N*M)
int bf(char s[], char p[], int n, int m) {
int i = 0, j = 0;
while (i < n && j < m) {
if (s[i] == p[j]) i++, j++;
else i = i - j + 1, j = 0;
}
return j == m ? i - j : -1;
}
// KMP,O(N + M)
int kmp(char s[], char p[], int n, int m) {
int ne[m];
ne[0] = -1, ne[1] = 0;
for (int i = 2, j = 0; i < m;) { // 求 next
if (p[i - 1] == s[j]) ne[i++] = ++j;
else if (j > 0) j = ne[j];
else ne[i++] = 0;
}
int i = 0, j = 0;
while (i < n && j < m) {
if (s[i] == p[j]) i++, j++;
else if (ne[j] == -1) i++;
else j = ne[j];
}
return j == m ? i - j : -1;
}
真题演练
2018 年 408 真题:主串 abaabaabacacaabaabcc
模式串 abaabc
,使用 KMP 进行匹配,第一次失配时,i = j = 5
,则下次匹配时 i
和 j
的值分别是多少?
abaabaabacacaabaabcc
^ 第六次匹配失败,i = 5
abaabc
^ "abaab",最长公共前后缀为 2
------------------------------------------------------------------------
abaabaabacacaabaabcc
^ 无需回退,i = 5
abaabc
^ 从前缀末尾的下一个元素开始。第三个元素,j = 2
答:i = 5,j = 2
2019 年 408 真题:主串 abaabaabcabaabc
模式串 abaabc
,使用 KMP 进行匹配,直到完全匹配一共需要比较多少次?
abaabaabcabaabc
^ 第六次匹配失败
abaabc
^ "abaab",最长公共前后缀为 2
------------------------------------------------------------------------
abaabaabcabaabc
^
abaabc
^
------------------------------------------------------------------------
abaabaabcabaabc
^
abaabc
^ 再匹配四次后越界,完成整个匹配
答:6 + 4 = 10 次