学习笔记之对KMP算法的理解
一,问题引入.
有两个字符串,s[0...n-1] 和 t[0...m-1] 在字符串s中找字符串t;
二,思考.
对于这个问题可以逐一比较,即:
s | s[p] | s[p+1] | s[p+2] | ... | s[i-1] | s[i] | ... | ... | s[n-1] |
t | t[0] | t[1] | t[2] | ... | t[j-1] | t[j] | ... | t[m-1] |
即 s[p...i-1] == t[0...j-1],而且 s[i] != t[j], 这个时候最自然的想法是把 t 串右移一位从 t[0] 比较,即:
s | s[p] | s[p+1] | s[p+2] | ... | s[i-1] | s[i] | ... | ... | s[n-1] |
t | t[0] | t[1] | ... | t[j-2] | t[j-1] | ... | t[m-1] |
但是如果我们已经知道了 t[0] != s[p+1] ,那么右移一位显然是不必要的操作,类似的,若要使“右移”这一操作有意义(设右移后要和 s[i] 比较的字符是 t[k],如下表),
则,必然要在 s[p...i-1](也就是已经匹配的部分) 中找到一个字符使得 s[i-k] == t[0] , 这时,如果不幸的事情发生(在已匹配部分有 s[i-k+x] != t[x] ),那么
又是一次无意义的右移,纯属浪费精力
显然有意义的右移必须满足 s[i-k...i-1] == t[0...k-1] ,即 :
s | s[p] | ... | s[i-k] | s[i-k+1] | ... | s[i-1] | s[i] | ... | ... | ... | ... | s[n-1] | ||
t |
t[0] | t[1] | ... | t[k-1] | t[k] | ... | t[j-1] | ... | t[m-1] |
因为 s[p...i-1] == t[0...j-1] ,而且 s[i-k...i-1] 是 s[p...i-1] 的后缀,所以也是 t[0...j-1] 的后缀, 又因为 t[0...k-1] 是 t[0...j-1] 的前缀,而且,t[0...k-1] == s[i-k...i-1] 所以 t[0...k-1] 是 t[0...j-1] 的前缀和后缀。问题又来了(真是命途多舛~_~),什么问题呢?如果对于 t[0...j-1] 有多个子串满足:既是前缀又是后缀,那应该取哪一个(k)呢 ?举个例子,t[0]t[1] == t[j-2]t[j-1] 和 t[0...j-2] == t[1...j-1], 若选取前者,那么 j = 3, 依然可以继续比较而且是有意义的右移,如果选取后者, j = j-1, 同样是有意义的右移,这有什么差别!!差别是必须有的,假如选取后者时匹配成功,存储匹配成功时 i 的值,然后继续比较,在这个假设下,对于前者所存储的 i 的值里显然是少了,因为 前者直接右移多位,从 s[i-2...]开始比较,忽略了 s[p+1...] 匹配成功的可能;所以应该选取最长的这种子串
那么假如已经找到了 k 呢?如果 s[i] == t[k] 自然是一路愉快的比较下去咯!如果不一样,,,也不是问题,把 k 当作刚才的 j 就好了 !
由前面的分析知道,不同的 j 对应 不同的 k ,所以可以设一个 next数组,使 k = next[j] ;
如果有了 next 数组:
1 void kmp()//kuangbin模板 2 { 3 char s[maxn], t[maxn] ; 4 int next[maxn] ; 5 gets(s) ; 7 gets(t) ; 8 get_next(t,next);//获取next数组的函数 ; 9 int i = 0, j = 0 ; 10 int len_s = strlen(s), len_t = strlen(t); 11 while (i < len_s) { 12 while (j != -1 && s[i] != t[j]) j = next[j] ; //下面求next数组时把next[0] 初始化为-1,这表明t【0】匹配失败时,需要 串s 左移 13 ++ i; 14 ++ j; 15 if (j == len_t) { 16 printf("%d ",i) ;//输出匹配成功时t[m-1]在串s中的序号(下标加一) ; 17 j = next[j] ;//匹配成功了再次重新匹配,可能在 串s有多个 串t ; 18 } 19 } 20 puts("\n") ; 21 }
上面代码的关键是 while (j != -1 && s[i] != t[j]) j = next[j] ; s[i] != t[j] 表明 j 时 匹配失败,这时 串t 右移 ,把 j 赋值为 next[j] (可以这样考虑, k = next[j], j = k ;)
若 j = -1, 也就是 k = -1 时,显然是不可以让 j = -1 的,这说明 t[0] 就匹配失败了,
s |
... | s[i] | ... | ... | s[n-1] | ||||
t | ... | t[0] | ... | t[m-1] |
即 : s[i] != t[0] ,这时候要这样比较咯 :
s | ... | s[i] | s[i+1] | ... | ... | s[n-1] | |||
t | ... | ... | t[0] | ... | t[m-1] |
即 ++ i; ++ j ;(这样 j 就 等于 0 了 ~_~) ;
新的问题来了,怎么获得这么好用的 next数组 呢 ?
三,next数组的获得.
接着分析啊!不想怎么可能知道 ~_~
突然间觉得前面一段话写错了位置:
因为 s[p...i-1] == t[0...j-1] ,而且 s[i-k...i-1] 是 s[p...i-1] 的后缀,所以也是 t[0...j-1] 的后缀, 又因为 t[0...k-1] 是 t[0...j-1] 的前缀,而且,t[0...k-1] == s[i-k...i-1] 所以 t[0...k-1] 是 t[0...j-1] 的前缀和后缀。问题又来了(真是命途多舛~_~),什么问题呢?如果对于 t[0...j-1] 有多个子串满足:既是前缀又是后缀,那应该取哪一个(k)呢 ?举个例子,t[0]t[1] == t[j-2]t[j-1] 和 t[0...j-2] == t[1...j-1], 若选取前者,那么 j = 3, 依然可以继续比较而且是有意义的右移,如果选取后者, j = j-1, 同样是有意义的右移,这有什么差别!!差别是必须有的,假如选取后者时匹配成功,存储匹配成功时 i 的值,然后继续比较,在这个假设下,对于前者所存储的 i 的值里显然是少了,因为 前者直接右移多位,从 s[i-2...]开始比较,忽略了 s[p+1...] 匹配成功的可能;所以应该选取最长的 这种子串;
写到这里才对嘛!
首先,按照前面说的,next[0] = -1;然后不用看 串t 是什么样的都能知道 next[1] = 0(t[1]匹配失败当然要从t[0]开始啦) ;完了~_~,next[2] 看不出来了 !
* 如果可以根据 next[1] 求 next[2] ,,,,这个还是先想想吧!容我先把 next[2] 算出来:
1, 若 t[0] == t[1] , 那么 next[2] = 1;
2, 若 t[0] != t[1] ,那么 next[2] = 0;
很好算嘛!Go on !
1, 若 t[0]t[1] == t[1]t[2], 那么 next[3] = 2 ;
2, 若 t[0]t[1] != t[1]t[2],那么 next[3] = ? 现在还不能确定,
3,若 t[0] == t[2] ,那么 next[3] = 1 ;
4, 若 t[0] != t[2] ,那么 next[3] = 0 ;
算next[3] 已经有点费力了(至少对我来说是这样的~_~),还是考虑前面的 * 吧!
考虑 由 next[j] 推出 next[j+1] ; 令 k = next[j] ;next[j] 表示 当 t[j] != s[i] 时,即匹配到 j 失败时 j 的新值
如表:
s | s[p] | ... | ... | ... | s[i-1] | s[i] | ... | ||||||||
t | t[0] | ... | t[j-k] | ... | t[j-1] | t[j] | ... | ||||||||
t | ... | ... | t[0] | ... | t[k-1] | t[k] | ... | t[j-1] | t[j] | ... |
即: t[0...k-1] == t[j-k...j-1] ;
如果 t[k] == t[j] ,那这个世界就太美好了! next[j+1] = next[j] + 1 ;
然而,,,,
好吧!看看 t[k] != t[j] 时;(CROSShh ! 再来一个表格)
t | t[0] | ... | t[k-1] | t[k] | ... | t[j-k] | ... | t[j-1] | t[j] | t[j+1] |
首先要明确目标: 寻找next[j+1](kk = next[j+1]) 即 t[0...j] 最长的一个子串(既是前缀又是后缀),因为 t[k] != t[j] 所以 next[j+1] 肯定比 k 小, (然并卵!)这表明 kk < k ,也就是只能在 t[0...k]寻找一个前缀使之等于 t[j-k...j]的一个后缀,即 t[0...kk-1] == t[j+1-kk...j] 等价于 t[0...kk-2] == t[j+1-kk...j-1] && t[kk-1] == t[j] ;把重点放在前面的式子 t[0...kk-2] == t[j+1-kk...j-1]
而且 t[k-kk+1...k-1] == t[j+1-kk...j-1] (由 t[0...k-1] == t[j-k...j-1] ),所以等价于 t[0...kk-2] == t[k-kk+1...k-1] && t[kk-1] == t[j]
t | t[0] | ... | t[kk-2] | t[kk-1] | ... | t[k-kk+1] | ... | t[k-1] | t[k] | ... | t[j] | t[j+1] | ... |
看前面的式子,t[0...kk-2] == t[k-kk+1...k-1] 这显然是 next[k] 啊!更新: k = next[k] 那么 kk-1 = k; 等价于判断 t[k] ?= t[j] ~_~貌似问题回来了~不是的!是递推了!!
代码附上:
1 void get_next(char a[],int next[]) 2 { 3 memset(next,0,sizeof(next)) ; 4 int k = -1, i = 0; 5 next[0] = -1; 6 int len = strlen(a) ; 7 while (i < len) { 8 while(k != -1 && a[i] != a[k]) k = next[k] ; 9 next[++i] = ++ k; 10 } 11 for (i = 0; i < len; i ++) 12 printf("%d ",next[i]) ; 13 puts("\n") ; 14 }
四,小试一把。(hdu 1686 oulipo)
这是一个赤裸裸的KMP,直接给代码好了
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 const int ma = 10010 ; 5 const int maxn = 1000010 ; 6 int ne[ma] ; 7 8 void get_next(char a[],int M) 9 { 10 memset(ne,0,sizeof(ne)) ; 11 int k = -1, i = 0 ; 12 ne[0] = -1 ; 13 while (i < M) { 14 while (k != -1 && a[i] != a[k]) k = ne[k] ; 15 ne[++i] = ++k; 16 } 17 return ; 18 } 19 20 int main() 21 { 22 int T ; 23 scanf("%d",&T) ; 24 char a[ma] , b[maxn] ; 25 while (T --) { 26 scanf("%s %s",a,b) ; 27 int le_a = strlen(a), le_b = strlen(b) ; 28 get_next(a,le_a) ; 29 int ans = 0 , i = 0, j = 0 ; 30 while (i < le_b && j < le_a) { 31 while (j != -1 && b[i] != a[j]) j = ne[j] ; 32 i ++ ; 33 j ++ ; 34 if (j >= le_a) { 35 ans ++ ; 36 j = ne[j] ; 37 } 38 } 39 printf("%d\n",ans) ; 40 } 41 return 0; 42 }
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。