KMP基本模板理解

对于一些基本的算法可以做一些题目记住模板:

对于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算法中,对于每一个模式串我们会事先计算出模式串的内部匹配信息,在匹配失败时最大的移动模式串,以减少匹配次数。
比如,在简单的一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配。右移的距离在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()函数代码:
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];   
    }
}
整理网上的一些题目与大佬的想法:

针对next的应用

裸的KMP也有几道入门题,HDU 1711   POJ 3461(直接套模板即可)

另外关于next的性质的两道题:

POJ 2406

利用next数组找字符串的循环节

[cpp]  view plain  copy
  1. #define mem(a,x) memset(a,x,sizeof(a))  
  2. #include<iostream>  
  3. #include<cstdio>  
  4. #include<cstring>  
  5. #include<algorithm>  
  6. #include<queue>  
  7. #include<set>  
  8. #include<stack>  
  9. #include<cmath>  
  10. #include<map>  
  11. #include<stdlib.h>  
  12. #include<cctype>  
  13. #include<string>  
  14. #define Sint(n) scanf("%d",&n)  
  15. #define Sll(n) scanf("%lld",&n)  
  16. #define Schar(n) scanf("%c",&n)  
  17. #define Sint2(x,y) scanf("%d %d",&x,&y)  
  18. #define Sll2(x,y) scanf("%lld %lld",&x,&y)  
  19. #define Pint(x) printf("%d",x)  
  20. #define Pll(x) printf("%lld",x)  
  21. #define Pintl(x) printf("%d\n",x)  
  22. using namespace std;  
  23. typedef long long ll;  
  24. /* 
  25.     结论:如果有一个字符串s,长度是len,它的失败函数是next, 
  26.     如果len能被len-next[len]整除, 
  27.     那么len-next[len]就是我们要求的那个子串的长度, 
  28.     与之对应的字符串,就是我们想得到的子串 
  29. */  
  30. const int N = 1000007;  
  31. int nx[N];  
  32. char s[N];  
  33. void getnx()  
  34. {  
  35.     int len = strlen(s);  
  36.     int i = 0,k = -1;  
  37.     nx[0] = -1;  
  38.     while (i < len)  
  39.     {  
  40.         if (k == -1||s[i] == s[k]) nx[++i] = ++k;  
  41.         else k = nx[k];  
  42.     }  
  43. }  
  44. int main()  
  45. {  
  46.     while (~scanf("%s",s))  
  47.     {  
  48.         if (s[0] == '.'break;  
  49.         getnx();  
  50.         int len = strlen(s);  
  51.         if (len%(len-nx[len]) == 0)  
  52.         {  
  53.             //Pintl((len-nx[len]));//所以这个是循环节的长度  
  54.             Pintl(len/(len-nx[len]));//这个是循环节的次数  
  55.         }  
  56.         else puts("1");  
  57.     }  
  58.     return 0;  
  59. }  

POJ 2752

又是利用了next数组的性质

[cpp]  view plain  copy
  1. #define mem(a,x) memset(a,x,sizeof(a))  
  2. #include<iostream>  
  3. #include<cstdio>  
  4. #include<cstring>  
  5. #include<algorithm>  
  6. #include<queue>  
  7. #include<set>  
  8. #include<stack>  
  9. #include<cmath>  
  10. #include<map>  
  11. #include<stdlib.h>  
  12. #include<cctype>  
  13. #include<string>  
  14. #define Sint(n) scanf("%d",&n)  
  15. #define Sll(n) scanf("%lld",&n)  
  16. #define Schar(n) scanf("%c",&n)  
  17. #define Sint2(x,y) scanf("%d %d",&x,&y)  
  18. #define Sll2(x,y) scanf("%lld %lld",&x,&y)  
  19. #define Pint(x) printf("%d",x)  
  20. #define Pll(x) printf("%lld",x)  
  21. #define Pintl(x) printf("%d\n",x)  
  22. using namespace std;  
  23. typedef long long ll;  
  24. /* 
  25.     从n - 1位既最后一位开始回滚, 
  26.     若s[next[n-1]] == s[n-1], 
  27.     则子串s[0,1,2,...,next[n-1]]是满足条件的子串。 
  28.     然后判断s[next[next[n-1]]] == s[n-1]是否成立, 
  29.     这样一直回滚,直到next[next[.....next[n-1]]] == -1为止。 
  30. */  
  31. const int N = 400007;  
  32. int nx[N];  
  33. char s[N];  
  34. int len;  
  35. void getnx()  
  36. {  
  37.     int i = 0,k = -1;  
  38.     nx[0] = -1;  
  39.     while (i < len)  
  40.     {  
  41.         if (k == -1||s[i] == s[k]) nx[++i] = ++k;  
  42.         else k = nx[k];  
  43.     }  
  44. }  
  45. int main()  
  46. {  
  47.     while(~scanf("%s",s))  
  48.     {  
  49.         len = strlen(s);  
  50.         getnx();  
  51.         int p = nx[len-1];  
  52.         vector<int>r;  
  53.         while (p+1)  
  54.         {  
  55.             if (s[p] == s[len-1]) r.push_back(p+1);  
  56.             p = nx[p];  
  57.         }  
  58.         for(int i = r.size()-1;i >= 0;--i)  
  59.         {  
  60.             Pint(r[i]);putchar(' ');  
  61.         }  
  62.         Pintl(len);  
  63.     }  
  64.     return 0;  
  65. }  

从网上淘来的比较厉害的模板代码:

  1. #define mem(a,x) memset(a,x,sizeof(a))  
  2. #include<bits/stdc++.h>  
  3. using namespace std;  
  4. typedef long long ll;  
  5. const int N = 1000007;  
  6.   
  7. struct KMP  
  8. {  
  9.     string s, t;    //s.size > t.size  
  10.     int slen, tlen;  
  11.     int nx[N];  
  12.     KMP (string s = "", string t = "") : s (s), t (t)   //构造函数  
  13.     {  
  14.         slen = s.size();  
  15.         tlen = t.size();  
  16.         mem (nx, 0);  
  17.     }  
  18.     void init (string s, string t)  
  19.     {  
  20.         this->s = s;  
  21.         this->t = t;  
  22.         slen = s.size();  
  23.         tlen = t.size();  
  24.         mem (nx, 0);  
  25.     }  
  26.     void getnx()  
  27.     {  
  28.         int i = 0, k = -1;  
  29.         nx[0] = -1;  
  30.         while (i < tlen)  
  31.         {  
  32.             if (k == -1 || t[i] == t[k]) nx[++i] = ++k;  
  33.             else k = nx[k];  
  34.         }  
  35.     }  
  36.     int findindex() //返回首次出现的位置  
  37.     {  
  38.         int i = 0, j = 0;  
  39.         getnx();  
  40.         while (i < slen&&j < tlen)  
  41.         {  
  42.             if (j == -1||s[i] == t[j]) ++i,++j;  
  43.             else j = nx[j];  
  44.         }  
  45.         if (j == tlen) return i - tlen;  
  46.         else return -1;//不匹配  
  47.     }  
  48.     int findtimes()//返回出现次数  
  49.     {  
  50.   
  51.         if (slen == 1&&tlen == 1)  
  52.         {  
  53.             return s[0] == t[0];  
  54.         }  
  55.         getnx();  
  56.         int times = 0;  
  57.         for (int i = 0,j = 0;i < slen;++i)  
  58.         {  
  59.             while (j > 0&&s[i]!=s[j]) j = nx[j];  
  60.             if (s[i] == t[j]) ++j;  
  61.             if (j == tlen)  
  62.             {  
  63.                 ++times;  
  64.                 j = nx[j];  
  65.             }  
  66.         }  
  67.         return times;  
  68.     }  
  69. };  
  70. int main()  
  71. {  
  72.   
  73.     return 0;  
  74. }  
根据一个完整的题目在理解一下KMP:
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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值