问题导入
给定一个文本串 和一个模式串 ,在 中找到所有 出现过的位置。
暴力匹配算法
想当然地考虑,我们可以从 的第一个字符开始,逐个与 的字符比较,如果相同,则继续比较下一个字符,否则从 的下一个字符开始重新比较。
实现如下:
vector<int> match(string S, string P) { int n = S.size(), m = P.size(), i = 0, j = 0; vector<int> ans; while (i < n) { if (S[i] == P[j]) { i++; j++; if (j == m) { ans.push_back(i - m); i -= j - 1; j = 0; } } else { i -= j - 1; j = 0; } } return ans;}易得,暴力匹配算法的时间复杂度为 。
KMP 算法
首先,我们注意到,在暴力匹配算法中,在最坏情况下,每次匹配失败后,我们都要回退到 这个位置来重新匹配。 而此时我们实际上已经知道了 的所有信息,我们完全没有必要重新获取一遍这些信息。 换句话说,我们能不能做到不让 指针左移?
举例:对于主串 和模式串 ,当我们比较 时,对于暴力匹配算法,我们接下来会比较 和 。 而事实上,我们已经知道了 和 的全部内容,经过观察,我们发现只需要继续比较 和 即可,因为 和 是完全匹配的。 那我们要怎么让计算机知道这件事呢?
先假设我们有一个 神奇数组 magic,它可以告诉我们,如果我们匹配失败了,根据已知信息,我们可以跳过多少字符,直接开始下一个字符的比较。
用更书面化的语言来说,这个神奇数组告诉了我们在当前匹配失败后,模式串指针可以跳转到哪里,保证前面的字符都是匹配的。
此时借助这个神奇数组和一些想当然,就可以得出以下算法:
vector<int> kmp(string S, string P) { vector<int> ans; vector<int> magic; int n = S.size(), m = P.size(), i = 0, j = 0; while (i < n) { if (S[i] == P[j]) { i++; j++; if (j == m) { ans.push_back(i - m); j = magic[j]; // 如果我们对模式串匹配成功了,可以假设是在模式串末尾加一个与任意字符都匹配失败的字符,对应的神奇数组跳转到的位置保证前面的字符都是匹配的。 } } else { if (j > 0) j = magic[j]; else i++; // 如果我们第一个字符就匹配失败了,那就只能从主串中下一个位置继续开始了 } } return ans;}由于主串指针的单调性,KMP 算法的时间复杂度是 的。
显然我们不能只靠假设就得出来神奇数组,做事情是要有根据的。
前缀函数
在学习怎么求神奇数组前,我们需要一点前置知识:前缀函数。
给定一个长度为 的字符串 ,其前缀函数定义为一个长度为 的数组 ,其中 被定义为子串 的最长的相等的真前缀与真后缀的长度。详尽具体的定义见前缀函数。
这里直接给出最优算法,详细推导证明省略。我们需要知道,这个算法可以在 的时间求得一个字符串的前缀函数。
vector<int> prefix_function(string s){ int n=s.size(); vector<int> pi(n); for(int i=1;i<n;i++){ int j=pi[i-1]; while(j>0&&s[i]!=s[j])j=pi[j-1]; if(s[i]==s[j])j++; pi[i]=j; } return pi;}注意到,如果一个字符串的前缀 和后缀 完全相同,在主串和模式串的匹配中,就可以借此性质来实现神奇数组的功能。 对于模式串指针 ,当我们在匹配失败时,若有 和 完全相同,那就可以跳过这一部分,直接在当前位置继续匹配 。
失配函数
在 KMP 算法中,我们把所谓的神奇数组叫做失配函数,通常指的是 KMP 匹配失败时,模式串应该跳转到哪里,常写成 next[i]、fail[i] 等形式。
从数值上来说,显然,我们有 。
举例:对于模式串 ,它的前缀函数为 ,它的失配函数为 。
我们令 表示对于主串当前位置的匹配彻底失败,应该从下一个位置再重新开始匹配。也可以想象成在模式串前面加一个与任意字符都匹配的字符。 而 则表示的是我们匹配成功后可以直接跳转到 处开始新的一轮匹配,而不一定要从第一位开始。
失配函数的求法可以先求出前缀函数再移项得到,也可以想象成模式串自己与自己匹配。类比 KMP 算法代码可以得出:
vector<int> failure_function(string s){ int n=s.size(); vector<int> fail(n+1); fail[0]=-1; int i=0,j=-1; while(i<n){ if(j==-1||s[i]==s[j]){ i++; j++; fail[i]=j; }else j=fail[j]; } return fail;}易得,求解失配函数的时间复杂度是 的。
算法模板
结合以上内容,我们可以得到一份 KMP 算法模板,综合来看,时间复杂度是 的。
vector<int> failure_function(string s){ int n=s.size(); vector<int> fail(n+1); fail[0]=-1; int i=0,j=-1; while(i<n){ if(j==-1||s[i]==s[j]){ i++; j++; fail[i]=j; }else j=fail[j]; } return fail;}vector<int> kmp(string S, string P) { vector<int> ans; vector<int> fail=failure_function(P); int n = S.size(), m = P.size(), i = 0, j = 0; while (i < n) { if (S[i] == P[j]) { i++; j++; if (j == m) { ans.push_back(i - m); j = fail[j]; } } else { if (j > 0) j = fail[j]; else i++; } } return ans;}部分信息可能已经过时
