对于一些基本的算法可以做一些题目记住模板:
对于KMP的理解主要是从书(数据结构-严蔚敏.吴伟民两位老师)上看到消化的;
kmp算法:
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
设主串(下文中我们称作T)为:a b a c a a b a c a b a c a b a a b b
模式串(下文中我们称作W)为:a b a c a b
用暴力算法匹配字符串过程中,我们会把T[0] 跟 W[0] 匹配,如果相同则匹配下一个字符,直到出现不相同的情况,此时我们会丢弃前面的匹配信息,然后把T[1] 跟 W[0]匹配,循环进行,直到主串结束,或者出现匹配成功的情况。这种丢弃前面的匹配信息的方法,极大地降低了匹配效率。
而在KMP算法中,对于每一个模式串我们会事先计算出模式串的内部匹配信息,在匹配失败时最大的移动模式串,以减少匹配次数。
在第一次匹配过程中
T: a b a c a
a b a c a b a c a b a a b b
W: a b a c a
b
在T[5]与W[5]出现了不匹配,而T[0]~T[4]是匹配的,现在T[0]~T[4]就是上文中说的
已经匹配的模式串子串,现在移动找出
最长的相同的前缀和后缀并使他们重叠:
T: a b a c a
ab a c a b a c a b a a b b
W: a
b a c a
b
然后在从上次匹配失败的地方进行匹配,这样就减少了匹配次数,增加了效率。
然而,如果每次都要计算
最长的相同的前缀反而会浪费时间,所以对于模式串来说,我们会提前计算出每个匹配失败的位置应该移动的距离,花费的时间就成了常数时间。
难点:
我认为比较难的地方就是next()函数这个地方,不能够理解清楚:
我整理了一下有关next()函数求法,希望能够清楚:
next[j]的含义:
结合前后缀的概念,可得出next[j]的的实际含义:next[j]就是模式串下标 0...j-1 这j个字符的最长相对应前缀和后缀的长度。
很显然,相对应是指可以匹配得上。至于为什么是最长?,基于两点考虑:
(i)直观上看,next[j]最大,说明剩下需要进行匹配验证的字符就最少嘛。
(ii)本质上,只能取最大,否则会遗漏可能的匹配。(这一点,需要仔细想想!)
需要指出,这里我们只能考虑非平凡的前后缀,否则,对于平凡的无意义。(平凡前后缀是指:空串和串本身。其它的都是非平凡的。)
还有一点我们得明白:next数组完全由模式串本身确定,与主串无关!
求解next数组的步骤:
1.先求出以当前字符结尾的子串的最长相对应前缀和后缀的长度。
2.当前字符的next值,需要参考(参考就是取的意思)上一个字符的步骤1中的求解结果。至于第一个字符,由于没有“上一个字符”的说法,直接设置为-1,即可。
可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)
A B C D A B D
部分匹配值 0 0 0 0 1 2 0
next -1 0 0 0 0 1 2
直观地看就是:把“最长前后缀长度表”右移一个位置,于是最右边的一个长度被丢弃掉了,最左边空出的填上-1。这样得到的就是next数组。
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;
”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,
- “A”的前缀和后缀都为空集,共有元素的长度为0;
- “AB”的前缀为[A],后缀为[B],共有元素的长度为0;
- “ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- “ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- “ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;
- “ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;
- “ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
移动位数 = 已匹配的字符数 - 对应的部分匹配值 ;
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。
结合前后缀的概念,可得出next[j]的的实际含义:next[j]就是模式串下标 0...j-1 这j个字符的最长相对应前缀和后缀的长度。
很显然,相对应是指可以匹配得上。至于为什么是最长?,基于两点考虑:
(i)直观上看,next[j]最大,说明剩下需要进行匹配验证的字符就最少嘛。
(ii)本质上,只能取最大,否则会遗漏可能的匹配。(这一点,需要仔细想想!)
需要指出,这里我们只能考虑非平凡的前后缀,否则,对于平凡的无意义。(平凡前后缀是指:空串和串本身。其它的都是非平凡的。)
还有一点我们得明白:next数组完全由模式串本身确定,与主串无关!
求解next数组的步骤:
1.先求出以当前字符结尾的子串的最长相对应前缀和后缀的长度。
2.当前字符的next值,需要参考(参考就是取的意思)上一个字符的步骤1中的求解结果。至于第一个字符,由于没有“上一个字符”的说法,直接设置为-1,即可。
可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)
A B C D A B D
部分匹配值 0 0 0 0 1 2 0
next -1 0 0 0 0 1 2
直观地看就是:把“最长前后缀长度表”右移一个位置,于是最右边的一个长度被丢弃掉了,最左边空出的填上-1。这样得到的就是next数组。
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;
”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,
- “A”的前缀和后缀都为空集,共有元素的长度为0;
- “AB”的前缀为[A],后缀为[B],共有元素的长度为0;
- “ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- “ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- “ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;
- “ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;
- “ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
移动位数 = 已匹配的字符数 - 对应的部分匹配值 ;
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。
先打一下next()函数代码:
void getnext()
{
ps=strlen(p);
//"i" 是记录进行next值计算的长度,会一直一个一个的加;
int i=0,j=-1; //初始化i,j;
next[0]=-1; //让next[0]==-1;
while(i<ps) //控制计算在模板串的范围内
{
if(j==-1||p[i]==p[j]) //满足条件
{
j++;
i++;
/**/if(p[i]==p[j])//这个是已经优化的代码
next[i]=next[j]; //满足条件将next[j]赋值;
/**/else
/**/ next[i]=j; //不满足时直接赋值“j”;
}
else
j=next[j];
}
}
ps=strlen(p);
//"i" 是记录进行next值计算的长度,会一直一个一个的加;
int i=0,j=-1; //初始化i,j;
next[0]=-1; //让next[0]==-1;
while(i<ps) //控制计算在模板串的范围内
{
if(j==-1||p[i]==p[j]) //满足条件
{
j++;
i++;
/**/if(p[i]==p[j])//这个是已经优化的代码
next[i]=next[j]; //满足条件将next[j]赋值;
/**/else
/**/ next[i]=j; //不满足时直接赋值“j”;
}
else
j=next[j];
}
}
整理网上的一些题目与大佬的想法:
针对next的应用
裸的KMP也有几道入门题,HDU 1711 POJ 3461(直接套模板即可)
另外关于next的性质的两道题:
利用next数组找字符串的循环节
POJ 2752
又是利用了next数组的性质
从网上淘来的比较厉害的模板代码:
https://vjudge.net/contest/177433#problem/E
E - KMP
#include<iostream>
#include<cstdio>#include<cstring>
#include<algorithm>
using namespace std;
char t[1000010],p[1000010];//定义两个字符串数组,主串与模板串;
int ts,ps; //用于存储两个字符串的长度
int next[10001]; //求next函数所用到的next[]数组
//getnext()函数
void getnext()
{
ps=strlen(p);
//"i" 是记录进行next值计算的长度,会一直一个一个的加;
int i=0,j=-1; //初始化i,j;
next[0]=-1; //让next[0]==-1;
while(i<ps) //控制计算在模板串的范围内
{
if(j==-1||p[i]==p[j]) //满足条件
{
j++;
i++;
/**/if(p[i]==p[j])//这个是已经优化的代码
next[i]=next[j]; //满足条件将next[j]赋值;
/**/else
/**/ next[i]=j; //不满足时直接赋值“j”;
}
else
j=next[j];
}
}
int KMP()
{
ts=strlen(t);
ps=strlen(p);
int i=0, j=0;
int sum=0;
while(j<=ps&&i<=ts)
{
if(t[i]==p[j]||j==-1)
{
i++;
j++;
//下面的if 语句只是为了求匹配到的次数;
/**/if(j==ps)
/**/ {
/**/ sum++;
/**/ j=next[j];
/**/ }
}
else
j=next[j];
}
//下面的这些注释掉的是最基本的模板;
//求第一次匹配到的位置;
//if(j==ps)
// return i-ps;
//else
//return -1;
return sum;
}
int main()
{
int N;
scanf("%d",&N);
while(N--)
{
scanf("%s%s",p,t);
getnext();
printf("%d\n",KMP());
}
return 0;
}