学习笔记之对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[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 }
时间: 2024-10-13 04:35:12

学习笔记之对KMP算法的理解的相关文章

学习笔记:Caffe上LeNet模型理解

学习笔记:Caffe上LeNet模型理解 Caffe中用的模型结构是著名的手写体识别模型LeNet-5(http://yann.lecun.com/exdb/lenet/a35.html).当年美国大多数银行就是用它来识别支票上面的手写数字的.能够达到这种商用的地步,它的准确性可想而知,唯一的区别是把其中的sigmoid激活函数换成了ReLU. 为什么换成ReLU,上一篇blog中找到了一些相关讨论,可以参考. CNN的发展,关键就在于,通过卷积(convolution http://deepl

型学习笔记5:C源码理解

型学习笔记5:C源码理解 http://jfsqhwhat2.eju.cn/ http://13660038501.i.sohu.com/v2/guestbook/index.htm http://15306736050.i.sohu.com/v2/guestbook/index.htm http://15090269366.i.sohu.com/v2/guestbook/index.htm http://13377896359.i.sohu.com/v2/guestbook/index.htm

KMP算法详细理解

KMP算法详细理解 从昨天开始看KMP算法到今天凌晨..... 把一些知识点进行总结,其实KMP还是挺简单的(HHHHHH) 博客新地址:https://miraitowa2.top/ 1:BF(暴力匹配)算法 假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢? 如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有: 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符: 如

KMP算法的理解

---恢复内容开始--- 在看数据结构的串的讲解的时候,讲到了KMP算法——一个经典的字符串匹配的算法,具体背景自行百度之,是一个很牛的图灵奖得主和他的学生提出的. 一开始看算法的时候很困惑,但是算法思想很简单,就是在暴力匹配的基础上得出的. 暴力匹配 这里有必要说一下暴力匹配,暴力匹配更简单,就是按照人的常规思维去匹配字符串,拿模式串(P)的第一个字符去和给定串(S)比较,S从左往右看,一看,第一个,呀~不对,啥也不说了,第一个都不对了,后边还比个毛.所以,这一次比较,S中第一个字符开头是匹配

KMP算法 --- 深入理解next数组

KMP算法的前缀next数组最通俗的解释 我们在一个母字符串中查找一个子字符串有很多方法.KMP是一种最常见的改进算法,它可以在匹配过程中失配的情况下,有效地多往后面跳几个字符,加快匹配速度. 当然我们可以看到这个算法针对的是子串有对称属性,如果有对称属性,那么就需要向前查找是否有可以再次匹配的内容. 在KMP算法中有个数组,叫做前缀数组,也有的叫next数组,每一个子串有一个固定的next数组,它记录着字符串匹配过程中失配情况下可以向前多跳几个字符,当然它描述的也是子串的对称程度,程度越高,值

KMP算法的理解和代码实现

KMP算法理解参考原文:http://kb.cnblogs.com/page/176818/ 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? 1. 首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较.因为B与A不匹配,所以搜索词后移一位. 2. 因为B与A不匹配,搜索词再往后

KMP算法初步理解

看了两天KMP算法,不知道理解的对不,初步理解总结如下:(主要是各种next数组把自己整晕了,有彻底懂的大神们再给指导下) 首先是思路,"字符串匹配的KMP算法_知识库_博客园"http://kb.cnblogs.com/page/176818/,问题的关键落在求数组上,而求数组实际是对自身求匹配度,所以求next数组的子函数和主函数很类似,所以网上讨论的好像主要是两种next数组,最好把相应的主函数列出来,还有像第二种的next和nextval数组都可用,在主函数相同的情况下,弥补一

字符串匹配KMP算法的理解(详细)

1. 引言 本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱.所以一直想找机会重新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文. 然近期因开了个算法班,班上专门讲解数据结构.面试.算法,才再次仔细回顾了这个KMP,在综合了一些网友的理解.以及算法班的两位讲师朋友曹博.邹博的理解之后,写了9张PPT,发在微博上.随后,一不做二不休,索性将PPT上的内容整理到了本文之中(后来文章越写越完整,所含内容早已不再是九张PPT 那样简单

LDA主题模型学习笔记5:C源码理解

1,说明 本文对LDA原始论文的作者所提供的C代码中LDA的主要逻辑部分做注释,代码可在这里下载:https://github.com/Blei-Lab/lda-c 这份代码实现论文<Latent Dirichlet Allocation>中介绍的LDA模型,用变分EM算法求解参数. 为了使代码在vs2013中运行做了一些微小改动,但不影响原代码的逻辑. vs2013工程可在我的资源中下载: http://download.csdn.net/detail/happyer88/8861773 -