KMP中最关键的就是通过getNext函数得到next数组。并且getNext函数也有一点点小区别,我想在此一一列举出来,作为模板给自己备用。
最近才理解了为什么单字符无前后缀:前缀不包括字符串的最后一个字符,后缀不包括字符串的第一个字符。
模板
模板一:
说明:next[i]存储的是第i+2个字符失配后要跳转的位置,其值等于长度为i+1的字符串,即p[0]p[1]……p[i]前后缀的最大相同长度。
在kmp函数中的使用,如果p[j]!=s[i],则j=next[j-1]。
这是next数组中没有-1元素的情况,适用于一般的模式匹配,不论是字符串还是整型数组。
int next[maxn];
void getNext(char* p, int len) {
next[0] = 0;
for (int i = 1; i < len; i++) {
int j = next[i - 1];
while (j > 0 && p[j] != p[i]) j = next[j - 1];
if (p[j] == p[i])
next[i] = j + 1;
else next[i] = 0;
}
}
来源:刘大有,杨波等的《数据结构》,其失败函数算法的改编。
原文如下:
算法 Fail(P. f) // 失败函数
F1.[赋初值]
m <- |P|. //获取串的长度
f(0) <- -1.
F2.[循环计算]
FOR j = 1 TO m - 1 DO
(i <- f(j-1).
WHILE p_j ≠ p_i+1 AND i >= 0 DO
i <- f(i).
IF p_j=p_i+1 THEN f(j) <- i+1.
ELSE f(i) <- -1.) ┃
模板二:
说明:next[i]存储的是第i+1个字符即p[i]失配后要回溯的位置,相当于模板一的next数组右移一个单位。next[i]值的大小为除去p[i]后的前i个字符组成的字符串,即p[0]p[1]……p[i-1]的最大前缀后缀的相同字符数。因此,next[len]也是有值的。
同样是没有-1存在的情况,在kmp函数中使用时,若p[j]!=s[i],则j=next[j].
理解next[0]==next[1]==0:第1个字符之前无字符,第2个字符之前只有1个字符,两者情况均无前缀后缀,因此默认为0。
int Next[maxn];
void getFail(char* p, int plen) {
//预计算Next[ ],用于在失配的情况下得到j回溯的位置
Next[0] = 0; Next[1] = 0;
for (int i = 1; i < plen; i++) {
int j = Next[i];
while (j && p[i] != p[j])j = Next[j];
Next[i + 1] = (p[i] == p[j]) ? j + 1 : 0;
}
}
来源:罗勇军,郭卫斌的《算法竞赛入门到进阶》。
模板三:
说明:该getNext的函数是经过优化的版本,匹配的速度可以更快一点。在上述两个模板中没有优化。
以模板一为例,若有字符串"aaabcdaaa",则next[0],next[1],next[2]是依次递增的,其值分别为0,1,2,如果p[2]=a和s[x]不匹配,则会跳到p[next[2-1]],即p[1],p[1]仍为a,显然也和s[x]不匹配,依次进行下去会发现到起点p[0]仍不匹配。
因此可以针对这点进行优化来减少几次循环。优化的方式为当p[i]==p[next[i]]时,next[i]=next[next[i]]。详细分析可以看来源博客。
//优化过后的next 数组求法
void GetNextval(char* p, int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++j;
++k;
//较之前next数组求法,改动在下面4行
if (p[j] != p[k])
next[j] = k; //之前只有这一行
else
//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
next[j] = next[k];
}
else
{
k = next[k];
}
}
}
对模板一和模板二进行优化,似乎得next数组完全生成后再进行,如果边生成边优化会使数组出问题。
for (int i = 1; i <= plen; i++) if (p[i] == p[Next[i]])Next[i] = Next[Next[i]];
入门习题+AC码
①HDU - 2087 剪花布条
分析:基础模板题。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1000 + 5;
int Next[maxn], cnt;
void getFail(char* p, int plen) {
//预计算Next[ ],用于在失配的情况下得到j回溯的位置
Next[0] = 0; Next[1] = 0;
for (int i = 1; i < plen; i++) {
int j = Next[i];
while (j && p[i] != p[j])j = Next[j];
Next[i + 1] = (p[i] == p[j]) ? j + 1 : 0;
}
}
void kmp(char* s, char* p) {
int sLen = strlen(s), pLen = strlen(p);
for (int i = 0, j = 0, last = 0; i < sLen; i++) {
while (j && p[j] != s[i])j = Next[j];
if (p[j] == s[i])j++;
if (j == pLen) {
cnt++;
if (sLen - last >= 2 * pLen) {
j = 0; last += pLen;
}
else return;
}
}
}
int main(void) {
char s[maxn], p[maxn];
while (~scanf("%s", s)) {
if (s[0] == '#')break;
cnt = 0;
scanf("%s", p);
kmp(s, p);
printf("%d\n", cnt);
}
return 0;
}
②HDU - 1686 Oulipo
分析:该题字符串匹配成功后回溯的地方应该是next数组中的对应元素,而非首字符。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxp = 1e4 + 5;
const int maxs = 1e6 + 5;
int next[maxp], ans;
char s[maxs],p[maxp];
void getNext(int len) {
next[0] = 0;
for (int i = 1; i < len; i++) {
int j = next[i - 1];//第i个字符要跳转的位置
while (j > 0 && p[j] != p[i]) j = next[j - 1];
if (p[j] == p[i])//找到的前缀长度为j,匹配第j+1和第i+1个字符
next[i] = j + 1;
else next[i] = 0;
}
}
void kmp() {
int slen = strlen(s), plen = strlen(p);
getNext(plen);
for (int i = 0, j = 0, last = 0; i < slen; i++) {
while (j && p[j] != s[i])j = next[j - 1];
if (p[j] == s[i])j++;
if (j == plen) ans++;
}
}
int main(void) {
int t;
scanf("%d", &t);
getchar();
while (t--) {
ans = 0;
scanf("%s%s", p, s);
kmp();
printf("%d\n", ans);
}
return 0;
}
③HDU - 1711 Number Sequence
分析:int数组的kmp,和char数组是一样的。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxp = 1e4 + 5;
const int maxs = 1e6 + 5;
int next[maxp], ans;
int s[maxs],p[maxp];
void getNext(int len) {
next[0] = 0;
for (int i = 1; i < len; i++) {
int j = next[i - 1];
while (j > 0 && p[j] != p[i]) j = next[j - 1];
if (p[j] == p[i])
next[i] = j + 1;
else next[i] = 0;
}
}
int kmp(int s[],int slen,int p[],int plen) {
getNext(plen);
int i = 0, j = 0;
while (j < plen && i < slen) {
while (j && p[j] != s[i])j = next[j - 1];
if (p[j] == s[i]) j++;
i++;
}
if (j < plen)return -2;
else return i - j;
}
int main(void) {
int t, slen, plen;
scanf("%d", &t);
while (t--) {
scanf("%d %d", &slen, &plen);
for (int i = 0; i < slen; i++)scanf("%d", &s[i]);
for (int i = 0; i < plen; i++)scanf("%d", &p[i]);
ans = kmp(s, slen, p, plen);
printf("%d\n", ans + 1);
}
return 0;
}
④POJ - 2406 Power Strings
分析:KMP算法在循环字符串上的应用,但是这题的数据直接暴力也是可以过的。
下图是重要知识点,简略证明可看来源博客。
图片来源:https://blog.csdn.net/qq_40679299/article/details/79837560
这种东西还是挺神奇的,感觉和数学定理一样,虽然正确但是不会证啊,背就完事了。
代码如下:
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1e6 + 5;
int next[maxn];
char p[maxn];
void getNext(char* p) {
int len = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < len) {
if (k == -1 || p[k] == p[j]) {
k++; j++;
if (p[k] != p[j])
next[j] = k;
else
next[j] = next[k];
}
else
k = next[k];
}
if (len % (len - next[len]) == 0)
printf("%d\n", len / (len - next[len]));
else
printf("1\n");
}
int main(void) {
while (~scanf("%s", p)) {
if (p[0] == '.')break;
getNext(p);
}
return 0;
}
⑤POJ - 1961 Period
分析:上一题的加强版,暴力求解是不能过了。如果理解了上一题,这个也是挺容易的。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1e6 + 5;
int next[maxn];
char p[maxn];
void getNext(char* p) {
int len = strlen(p);
next[0] = 0;
for (int i = 1; i < len; i++) {
int j = next[i - 1];
while (j > 0 && p[i] != p[j])j = next[j - 1];
if (p[i] == p[j])next[i] = j + 1;
else next[i] = 0;
if ((i + 1) % (i + 1 - next[i]) == 0)
if ((i + 1) / (i + 1 - next[i]) > 1)
printf("%d %d\n", i + 1, (i + 1) / (i + 1 - next[i]));
}
}
int main(void) {
int kase = 0, len;
while (~scanf("%d", &len) && len) {
printf("Test case #%d\n", ++kase);
scanf("%s", p); getNext(p);
printf("\n");
}
return 0;
}