今天复习到 KMP 算法时候发现考研教材上的 Next 数组和我之前学的不一样,所以特地拎出来比对一下,顺便总结。
因为我之前掌握的 KMP 是李煜东的《算法竞赛进阶指南》上介绍的,而考研教材似乎更多用的是严蔚敏的《数据结构(C语言版)》,所以就对这两种实现方式做整理。
设 P[i , j] 为模式串 P 从 i 到 j 位置上元素构成的子串,t 为当前子串中真前缀与真后缀的最长匹配长度。
假设字符串从下标 1 开始存放,即 P[1 , n] 为模式串。
nex[i] 表示“P 中以 i 结尾的真后缀”与“P 的真前缀”能够匹配的最长长度,即:nex[i] = max{ t },其中 t < i 并且 P[i-t+1, i] = P[1 , t] 。
亦即,在 P[ 1 , j ] 中长度为 t 的真前缀,应与长度为 t 的真后缀完全匹配,故 t 必来自集合: Next(P, j) = { 0 <= t < j | P[1 , t] = P[j - t + 1 , j] }
。
nex 数组的求法
代码块
void getNex(const char *s){
/*更新模式串s的nex数组*/
int len = strlen(s);
memset(nex,0,sizeof nex);
for(int i = 2,j = 0;i < len;i++){
while(j > 0 && s[i] != s[j+1]) j = nex[j];
if(s[i] == s[j+1]) j++;
nex[i] = j;
}
}
需要注意的是,该代码无需优化,在算法竞赛中该效率已足够优秀,为O(M+N)。
举例:
P | A | B | A | B | A | B | A | A |
---|---|---|---|---|---|---|---|---|
Next | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 1 |
可以使用任意字符串,并调用上述代码求对应的 Next 数组。
设 P[i , j) 为模式串 P 从 i 到 j-1 位置上元素构成的子串,t 为当前子串中真前缀与真后缀的最长匹配长度。
假设字符串从下标 1 开始存放,即 P[1 , n] 为模式串。
nex[i] 表示“P 中以 i 结尾的真后缀”与“P 的真前缀”能够匹配的最长长度再加 1,即:nex[i] = max{ t } +1,其中 t < i 并且 P[i-t, i) = P[1 , t] 。特别的,nex[1] = 0,因为 P[1,1) 是个空串。
亦即,在 P[ 1 , j ] 中长度为 t 的真前缀,应与长度为 t 的真后缀完全匹配,故 t 必来自集合: Next(P, j) = { 0 <= t < j | P[1 , t] = P[j - t , j) }
。
nex 数组的求法
代码块
void getNex(const char *s){
/*更新模式串s的nex数组*/
int len = strlen(s);
memset(nex,0,sizeof nex);
for(int i = 2,j = 0;i < len;i++){
while(j > 0 && s[i-1] != s[j]) j = nex[j];
j++; nex[i] = j;
}
}
举例与练习
下面通过一个例子表述如何手工写严版 next 表:
例如模式串为:AAAABAA
那么列表就有:
j = 1 为空串,此时定义nex[j] = 0;
A j = 2 有1个字符,此时真前缀与真后缀最长匹配长度为0,故nex[j] = 0+1;
AA j = 3 有2个字符,此时真前缀与真后缀最长匹配长度为1,故nex[j] = 1+1;
AAA j = 4 有3个字符,此时真前缀与真后缀最长匹配长度为2,故nex[j] = 2+1;
AAAA j = 5 有4个字符,此时真前缀与真后缀最长匹配长度为3,故nex[j] = 3+1;
AAAAB j = 6 有5个字符,此时真前缀与真后缀最长匹配长度为0,故nex[j] = 0+1;
AAAABA j = 7 有6个字符,此时真前缀与真后缀最长匹配长度为1,故nex[j] = 1+1;
所以如果理解了真前缀与真后缀,写next表就很简单。
next表的优化
所谓的优化就是为了避免移动后仍然失配的情况,因为每次 T[i] 和 P[j] 失配后,我们是令 j = nex[j],然后再继续比较 T[i] 和 P[j] 。因此一旦 T[i] 和 P[j] 失配时我们是一定知道 T[i] 不等于 P[j] 的, 此时如果 P[nex[j] ] = P[j] ,那显而易见还是会失配,于是我们就要重复 j = nex[j] 直至匹配成功或算法结束,所以我们就可以在计算 next 表的时候顺便判断一下转移前后 P 的字符变不变。
具体做法是:
void getNex(const char *s){
/*更新模式串s的nex数组*/
int len = strlen(s);
memset(nex,0,sizeof nex);
for(int i = 2,j = 0;i < len;i++){
while(j > 0 && s[i-1] != s[j]) j = nex[j];
j++;
if(s[j] != s[i]) nex[i] = j;
else nex[i] = nex[j];
}
}
求优化后的 next 表,也叫 nextval 表,操作只需要在求普通的 next 表上加一步,即在求出真前缀与真后缀的最长匹配长度 t 后,判断 P[t+1] 是否等于 P[i] ,如果相等就令 nextval[i] = nextval[t+1] ,否则就 nextval = t + 1。
例如:求AAAABAA 的 nextval(0 0 0 0 4 0 0 )
P:AAAABAA
j = 1 为空串,此时定义nex[j] = 0;
A j = 2 有1个字符,t = 0,且P[t+1] == P[j],故nex[j] = nex[1] = 0;
AA j = 3 有2个字符,t = 1,且P[t+1] == P[j],故nex[j] = nex[2] = 0;
AAA j = 4 有3个字符,t = 2,且P[t+1] == P[j],故nex[j] = nex[3] = 0;
AAAA j = 5 有4个字符,t = 3,且P[t+1] != P[j],故nex[j] = 3+1;
AAAAB j = 6 有5个字符,t = 0,且P[t+1] == P[j],故nex[j] = nex[1] = 0;
AAAABA j = 7 有6个字符,t = 1,且P[t+1] != P[j],故nex[j] = nex[2] = 0;