深度剖析KMP,让你认识真正的Next
KMP算法,想必大家都不陌生,它是求串匹配问题的一个经典算法(当然如果你要理解成放电影的KMP,请退出本页面直接登录各大电影网站,谢谢),我想很多人对它的理解仅限于此,知道KMP能经过预处理然后实现O(N*M)的效率,比brute force(暴力算法)更优秀等等,其实KMP算法中的Next函数,功能十分强大,其能力绝对不仅仅限于模式串匹配,它并不是KMP的附属品,其实它还有更多不为人知的神秘功能^_^
先来看一个Next函数的典型应用,也就是模式串匹配,这个相信大家都很熟悉了:
POJ 3461 Oulipo——很典型的模式串匹配问题,求模式串在目标串中出现的次数。
#include < iostream >
#include < cmath >
#include < cstring >
#include < algorithm >
using namespace std;
#define MAX 1000001
char t[MAX];
char s[MAX];
int next[MAX];
inline void calnext( char s[], int next[])
{
int i;
int j;
int len=strlen(s);
next[0]=-1;
j=-1;
for(i=1;i<len;i++)
{
while(j>=0&&s[i]!=s[j+1])
j=next[j];
if(s[j+1]==s[i])//上一个循环可能因为j=-1而不做,此时不能知道s[i]与s[j+1]的关系。故此需要此条件。
j++;
next[i]=j;
}
}
int KMP( char t[], char s[])
{
int ans=0;
int lent=strlen(t);
int lens=strlen(s);
if(lent<lens)
return 0;
int i,j;
j=-1;
for(i=0;i<lent;i++)
{
while(j>=0&&s[j+1]!=t[i])
j=next[j];
if(s[j+1]==t[i])
j++;
if(j==lens-1)
{
ans++;
j=next[j];
}
}
return ans;
}
int main()
{
int testcase;
scanf("%d",&testcase);
int i;
for(i=1;i<=testcase;i++)
{
scanf("%s%s",s,t);
calnext(s,next);
printf("%d\n",KMP(t,s));
}
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————
POJ 2406 Power Strings
这道题就比较有意思了,乍看之下,怎么看貌似都与KMP无关,呵呵,这就是因为你没有深入理解Next 的含义;
我首先来解释下这道题的题意,给你一个长度为n的字符串,首先我们找到这样一个字符串,这个字符串满足长度为n的字符串是由这个字符串重复叠加得到并且这个字符串的长度要最小.,然后输出重复的次数。
比如说,ababab这个字符串,显然它是由ab重复叠加3次得到,所以,答案输出3.
那么这个题用next怎么做呢,我们必须知道,next数组里面存放的是 如果当前匹配失败,模式串可以继续进行匹配的下一个位置。
For Example:
1 2 3 4 5 6
S= a b a b a ?
Next= 0 0 1 2 3 ?
其中next[5]=3,说明如果模式串在j=6处匹配失败,那么j=next[5]=3 ,为什么? 因为S的头三位和末三位是一样的,如果说S已经匹配到5的位置(匹配到5说明前5位都已经匹配上),那么前三位肯定也匹配上了(废话~),若果这个时候要继续匹配,我们可以将模式串向右平移几个个单位,这样保证S的前三位仍然是可以匹配上的。
比如说目标串是
i= 1 2 3 4 5 6 7 8 9
a b a b a d e f g
a b a b a c
j= 1 2 3 4 5 6
next= 0 0 1 2 3 ?
现在我们发现i=6与j=6两串不匹配怎么办?由于next[5]指向3,那么我们将模式串平移
i= 1 2 3 4 5 6 7 8 9
a b a b a d e f g
a b a b a c
j= 1 2 3 4 5 6
next= 0 0 1 2 3 ?
这样我们从j=3处继续向后匹配。(当然如果此时i=6与j=4仍然不匹配,那么我们继续使用失败函数,去寻找下一个位置)
好的,现在我们已经知道了,next数组中所存放的数字的含义,那么接下来让我们来看看怎样灵活的使用next,揭开它不为人知的另一面吧。
回到原题,原题要求求出一个字符串的某一个子串,使得这个字符串不断自我叠加后得到原串,并且这个重复的次数最多。那么它和next有什么关系呢???
结论:如果有一个字符串s,长度是len,它的失败函数是next,如果len能被len-next[len]整除,那么len-next[len]就是我们要求的那个子串的长度,与之对应的字符串,就是我们想得到的子串;
为什么呢? 假设我们有一个字符串ababab,那么next[6]=4对吧,由于next的性质是,匹配失败后,下一个能继续进行匹配的位置,也就是说,把字符串的前四个字母,abab,平移2个单位,这个abab一定与原串的abab重合(否则就不满足失败函数的性质),这说明了什么呢,由于字符串进行了整体平移,而平移后又要重叠,那么必有
s[1]=s[3],s[2]=s[4],s[3]=s[5],s[4]=s[6].说明长度为2的字符串在原串中一定重复出现,这就是len-next[len]的含义!
解决上面这个问题的同时,其实还有另一个问题,那就是如果这个字符串长度为奇数怎么办?如ababa,好像next[len]也等于3呢,可是aba-ba-似乎并不是由ba重复得到的吧。我们先把这个字符串断开,ab-ab-a,可以想象,中间的ab平移后,没有ab与它重合(只能重合一个,这虽然没有违背next的性质,但是却对本题的方法造成了影响,请读者细细品味),所以才会出现上面的情况!所以要加上len能够整除len-next[len]这个条件.
此题源代码如下:
// coded by abilitytao
// 2009年7月31日11:17:34
#include < cstdio >
#include < iostream >
#include < cmath >
#include < cstring >
#include < algorithm >
using namespace std;
#define MAX 1000001
int next[MAX];
inline void calnext( char s[], int next[])
{
int i;
int j;
int len=strlen(s);
next[0]=-1;
j=-1;
for(i=1;i<len;i++)
{
while(j>=0&&s[i]!=s[j+1])
j=next[j];
if(s[j+1]==s[i])
j++;
next[i]=j;
}
}
int main()
{
int n;
char str[MAX];
int len;
while(scanf("%s",&str))
{
if(str[0]=='.')
break;
len=strlen(str);
calnext(str,next);
if((len)%(len-1-next[len-1])==0)
printf("%d\n",(len)/(len-1-next[len-1]));
else
{
putchar('1');
putchar('\n');
}
}
return 0;
}
POJ 1961与上题类似,故不赘述,代码如下:
// coded by abilitytao
// 2009年7月31日10:56:07
#include < cstdio >
#include < iostream >
#include < cmath >
#include < cstring >
#include < algorithm >
using namespace std;
#define MAX 1000001
int next[MAX];
void calnext( char s[], int next[])
{
int i;
int j;
int len=strlen(s);
next[0]=-1;
j=-1;
for(i=1;i<len;i++)
{
while(j>=0&&s[i]!=s[j+1])
j=next[j];
if(s[j+1]==s[i])
j++;
next[i]=j;
}
}
int main()
{
int n;
char str[MAX];
int casenum=0;
int i;
int len;
while(scanf("%d",&n))
{
casenum++;
if(n==0)
break;
scanf("%s",str);
len=strlen(str);
printf("Test case #%d\n",casenum);
calnext(str,next);
for(i=1;i<len;i++)
{
if((i+1)%(i-next[i])==0&&next[i]!=-1)
printf("%d %d\n",i+1,(i+1)/(i-next[i]));
}
printf("\n");
}
return 0;
}
POJ 2752 Seek the Name, Seek the Fame
这道题揭开了next的另一个应用^_^
题目的意思可以这样描述:给出一个字符串S,长度为len;找出一个前缀一个后缀,使得这两个字符串相同。 输出所有可能的情况。
如aaaaa,
aaaaa ——》OK
aaaaa ——》OK
aaaaa + aaaaa ——》OK
aaaaa + aaaaa ——》OK
aaaaa + aaaaa ——》OK
那么这个题怎么用next呢,其实很简单,只要你知道next的含义。 s[1]——s[next[len]]中的内容一定能与s[1+len-next[len]]——s[len]匹配,所以s[1]——s[next[len]]就是我们要求取的最长的那个串,然后呢我们循环地利用next,由于next的性质,可以保证,每一次得出的字串都能匹配到最后一个字母,也就是得到一个前缀等于后缀。只不过这个字符串的长度在不断地减小罢了。不断地使用next我们直到求出所有的前缀^_^ So the problem is cleared.
附源代码:
// coded by abilitytao
// 2009年7月31日12:17:45
#include < cstdio >
#include < iostream >
#include < cmath >
#include < cstring >
#include < algorithm >
using namespace std;
#define MAX 1000001
int next[MAX];
inline void calnext( char s[], int next[])
{
int i;
int j;
int len=strlen(s);
next[0]=-1;
j=-1;
for(i=1;i<len;i++)
{
while(j>=0&&s[i]!=s[j+1])
j=next[j];
if(s[j+1]==s[i])//上一个循环可能因为j=-1而不做,此时不能知道s[i]与s[j+1]的关系。故此需要此条件。
j++;
next[i]=j;
}
}
int record[MAX];
int main()
{
char str[MAX];
int len;
int p=0;
int j;
while(scanf("%s",&str)!=EOF)
{
calnext(str,next);
p=0;
len=strlen(str);
j=len-1;
while(j!=-1)
{
record[++p]=j;
j=next[j];
}
while(p>1)
{
printf("%d ",record[p]+1);
p--;
}
printf("%d\n",record[p]+1);
}
return 0;
}
文章写完了,如果有什么疏漏,还请大家批评指正~
文章来自abilitytao博客
转载请注明出处: http://www.cppblog.com/abilitytao/archive/2009/08/01/91865.html