KMP算法模板+入门习题+AC码

KMP中最关键的就是通过getNext函数得到next数组。并且getNext函数也有一点点小区别,我想在此一一列举出来,作为模板给自己备用。

重点文章:从头到尾彻底理解KMP(2014年8月22日版)

最近才理解了为什么单字符无前后缀:前缀不包括字符串的最后一个字符,后缀不包括字符串的第一个字符。

模板

模板一:

说明: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];
		}
	}
}

来源:从头到尾彻底理解KMP(2014年8月22日版)

对模板一和模板二进行优化,似乎得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;
}
发布了104 篇原创文章 · 获赞 97 · 访问量 4515

猜你喜欢

转载自blog.csdn.net/TK_wang_/article/details/105307707