「KMP」学习笔记

前言—— charstring

  1. 有的时候 char 数组确实比 string 好用,且字符串长度很大时 string 会被卡掉,所以不要犯懒,老实用 charstring 可以用但是慎用。

  2. 同时很多情况下为了方便和减少出错,我们会想办法把字符串的坐标从 0len1 变成 1len ,对于 charstring 都有办法,但不尽相同。

    • char:
      cin>>s+1;
      int len=strlen(s+1);
    • string:
      cin>>s;
      s=" "+s;
      int len=s.size()-1;
      cin>>s;
      int len=s.size();
      s=" "+s;

定义与基本求法

  • 定义:

    用于匹配两字符串时的大幅度优化、border 问题、模式串在主串出现的次数以及位置等一系列问题,应用广泛,下面会依次解释。

    • |s|: 字符串 s 的长度。

      sub(l,r): 区间 (l,r) 子串的长度。

    • pre(s,i): s 长度为 r 的前缀。

      suf(s,i): s 长度为 r 的后缀。

    • border(经常应用 border 的性质 )

      0r<|s|,pre(s,r)=suf(s,r) ,则称 pre(s,r)border

      eg: ababababab,abab,ababab 均为其 border 。其中前后缀追均为严格意义上,长度小于总串长度的前后缀。

    • next 数组:(重中之重)

      1. 又名前缀表, next[i] 表示 pre(s,i) 的最长 border 长度。(基本定义)

      2. next[i] 表示两字符进行匹配,到该元素匹配失败时,重新匹配调到的位置,避免从 0 开始重新匹配。故此 next[i] 作为 i 的备选存在。

      3. pre(s,next[i]) 一定是 pre(s,i)border ;由此,pre(s,next[n]) 一定是 sbordern 表示 s 的长度 )。

        以上均可以根据其基本定义和 border 的性质得出。

  • 基本求法:

    1. 和自己匹配——求 next[i]

      解决模式串匹配主串问题时,需要先处理出模式窜的 next 数组。

      顾名思义,就是和自己匹配.

      先定义一个 i,j ,先用 sj+1 区匹配 sii2 开始, j0 开始。因为 next[1] 显然 =0

      若当前匹配失败且 j0 ,根据 next[j] 的基本定义,作为 j 的备选,另 j 不断跳 next[j] ,直到 si=sj+1 ,那么此时匹配成功,j++,next[i]=j 。如果一直跳到 j=0 还不能满足,便是匹配不上了,当前 next[i]=0

      明确一个问题,在不断跳 j=next[j] 的过程中,跳到 sj+1=si 时,此时得到的这个 pre(s,j) 必定是 pre(s,i1)border ,现在又满足 sj+1=s[i] ,那么 pre(s,j+1) 就成了 pre(s,i)border ,且一定是最长的 border ,即 next[i]

      通过上述方式从前往后枚举 i,枚举到 i+1 时, j 原先值保留,此时 j=next[i1] ,从而方便继续向前跳和接下来的步骤,这里需详细理解一下上一段文字。

      打个比方,如 aabaaf

      1. a ,显然 next[1]=0
      2. aas0+1=s2,j=1,next[2]=1
      3. aabs1+1s3,不断往前跳 j=next[j] ,始终不存在 sj+1=s3 ,故 next[3]=0
      4. aaba ,现在经历过上一步的跳 next 使 j=0s0+1=s4 ,故 j=1,next[4]=1
      5. aabaas1+1=s5,j=2,next[5]=2
      6. aabaafs2+1s6 ,不断向前跳 j=next[j] ,和第三次操作一样,始终不满足 sj+1=s6 ,故 j=0,next[6]=0

      也就得到了该串的 next 数组,即前缀表,同时表示 pre(s,i) 的最长 border 长度 :

      image

      • 代码如下:

        void kmp()
        {
        int j=0,l=strlen(s+1);
        for(int i=2;i<=l;i++)
        {
        while(j&&s[j+1]!=s[i]) j=nxt[j];
        if(s[i]==s[j+1]) j++;
        nxt[i]=j;
        }
        }
    2. 和主串匹配

      在此带入一道例题的情景,当然 kmp 的作用还有好多,下面的例题中还会有一定涉及。主串 s ,模式串 t

      image

      现已经将模式串的 next 处理出来,那么匹配主串就是轻而易举的了。

      先来看一下暴力是怎么匹配的:

      image

      可以看的出,每次匹配失败后,就从头开始重新匹配。

      但使用 kmp 遍不用这样。

      依旧是上述的 i,j ,当匹配 sitj+1 时,如果匹配失败, 遍不断往前跳 next 直至可以匹配,思路和打法几乎和求 next 是完全一样的。

      如上面的例子,采用 kpm 就可以:

      image

      而不必从头开始。

      那么这道题要求出现的次数,那么每次 j 匹配到 m 时,也就表示模式串匹配完一遍了,记录答案 ans++ ,另 j=nxt[m] 继续匹配即可。( m 表示模式串的长度 )。

      • 代码如下:

        int ask(string s,string t)
        {
        int j=0,n=s.size()-1,m=t.size()-1,ans=0;
        for(int i=1;i<=n;i++)
        {
        while(j&&t[j+1]!=s[i]) j=nxt[j];
        if(s[i]==t[j+1]) j++;
        if(j==m) ans++,j=nxt[j];
        }
        return ans;
        }
    3. 子串周期循环问题。

      该问题下面的例题中会有详细描述,需要注重理解好 nextborder 的含义。

  • 关于复杂度

    玄学玩意,虽然有个 while 但最多执行 n 次,最后还是 O(n)

    看一下课件吧:

    image

例题

OKRPeriodsofWords

  • 题目链接

  • 题面:

    对于一个串 s ,存在一个子串(长度小于主串)周期,例如 ab,abab,ababab 均为 abababab 的周期,其中 ababab 为最长周期,而 abc 没有周期,则最长周期长度为 0 。给定一个字符串 s ,求其所有前缀的最大周期长度之和。

  • 解法:

    先来看一张图:

    image

    也就完美的解释了这道题,这样的话就不断跳 next[i] ,使得到 >0 的最小的一个 next 设其为 jans+=j 即可,当然如果他的 next 最大就是 0 了,ans+=0

  • 代码如下:

    #include<bits/stdc++.h>
    #define int unsigned long long
    #define endl '\n'
    using namespace std;
    const int N=1e6+10,P=1e9+7;
    template<typename Tp> inline void read(Tp&x)
    {
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
    }
    int n,ans,nxt[N];
    char s[N];
    void kmp()
    {
    int j=0,l=strlen(s+1);
    for(int i=2;i<=l;i++)
    {
    while(j&&s[j+1]!=s[i]) j=nxt[j];
    if(s[i]==s[j+1]) j++;
    nxt[i]=j;
    }
    }
    signed main()
    {
    #ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n);
    cin>>(s+1);
    kmp();
    for(int i=2;i<=n;i++)
    {
    int j=i;
    while(nxt[j]) j=nxt[j];
    if(nxt[i]) nxt[i]=j;
    ans+=i-j;
    }
    cout<<ans;
    }
  • 扩展:如果求最小周期呢?

    根据上面的题不难相出,改成最大的 next 就可以了,其实就是直接的 next[i] 。然后 ans+=inext[i] 即可,似乎更简单一点,但我们仍应该证明一下。

    image

    其实也就是这道题:Radio Transmission

    • 代码如下:

      #include<bits/stdc++.h>
      #define int long long
      #define endl '\n'
      using namespace std;
      const int N=1e6+10,P=1e9+7;
      template<typename Tp> inline void read(Tp&x)
      {
      x=0;register bool z=1;
      register char c=getchar();
      for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
      for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
      x=(z?x:~x+1);
      }
      int n,nxt[N];
      string s;
      void kmp(string s)
      {
      int j=0;
      for(int i=2;i<=n;i++)
      {
      while(j&&s[j+1]!=s[i]) j=nxt[j];
      if(s[i]==s[j+1]) j++;
      nxt[i]=j;
      }
      }
      signed main()
      {
      #ifndef ONLINE_JUDGE
      freopen("in.txt","r",stdin);
      freopen("out.txt","w",stdout);
      #endif
      read(n);
      cin>>s;
      s=" "+s;
      kmp(s);
      cout<<n-nxt[n];
      }

动物园

  • 题目链接

  • 题面:

    给定一字符串 s ,求其每一个前缀的长度 <len2border 的个数。( len 指该前缀的长度 )

  • 解法:

    在此处换一种想法,不一定非要求自身的个数,对于一个 si ,我们求其后面可能出现的 sjnum ,此处 sj 可以通过跳 next 跳到 si 的位置,且 i 为其跳 next 过程中第一个 <j2 的位置。

    可能听起来不太好理解,就比方说,我现在是 si ,那么我的后面将有一个 sj 需要我,那么我将要给 sj 贡献多少的 num

    不同于题面,重新定义 numi 表示 si 将为 sj 贡献的值,继续上面的情景,既然我是他跳 next 跳过来的,那么我一定能和他的后缀构成 border ,那么到我这里,他将继续向前跳一直到 0 ,那么此时他往前继续跳的 next 也一定是我的 next ,既然到我这里已经 <j2 了,那么我前面的一定也满足,我不妨将我前面 next 的数量算上我自己一起给他,这样他就不用费劲的向前跳了。(就不会 TLE 了)

    看到这里好像发现了,就是对于每一个长度为 j 的前缀,他不断跳 next ,当他跳到 <j2 时,再往前跳多少步跳到 0 ,就是他的 ans 值,把这些 ans 加起来就是最后要求的值。

    那么思考上面的情景,每一个 si 他的 numi 就是他不断往前跳 next 跳多少次到 0 。又发现 numi=numnext[i]+1 ,于是可以线性求,在处理 next 数组时可以顺便求出来。

  • 代码如下:

    #include<bits/stdc++.h>
    #define int unsigned long long
    #define endl '\n'
    using namespace std;
    const int N=1e6+10,P=1e9+7;
    template<typename Tp> inline void read(Tp&x)
    {
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
    }
    int n,nxt[N],num[N];
    char s[N];
    void kmp()
    {
    int j=0,l=strlen(s+1);
    num[1]=1;
    for(int i=2;i<=l;i++)
    {
    while(j&&s[j+1]!=s[i]) j=nxt[j];
    if(s[j+1]==s[i]) j++;
    nxt[i]=j;
    num[i]=num[j]+1;
    }
    }
    int ask()
    {
    int j=0,l=strlen(s+1),ans=1;
    for(int i=2;i<=l;i++)
    {
    while(j&&s[j+1]!=s[i]) j=nxt[j];
    if(s[j+1]==s[i]) j++;
    while(j>(i/2)) j=nxt[j];
    ans=ans*(num[j]+1)%P;
    }
    return ans;
    }
    signed main()
    {
    #ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n);
    while(n--)
    {
    memset(nxt,0,sizeof(nxt));
    cin>>s+1;
    kmp();
    cout<<ask()<<endl;
    }
    }

剪花布条

  • 剪花布条

  • 题面:

    和模式串与主串的匹配十分类似,不同的是每个匹配不可重叠:

    eg: aaaa 直接匹配 aa 应是 3 个,但此处顾名思义 “剪”,所以只能剪出来 2 个。

  • 解法:

    与基本求法中的匹配十分相似,只需要在匹配完一遍后不让 j=next[j] ,而是让 j=0 即可。

  • 代码如下:

    #include<bits/stdc++.h>
    #define int long long
    #define endl '\n'
    using namespace std;
    const int N=1e6+10,P=1e9+7;
    template<typename Tp> inline void read(Tp&x)
    {
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
    }
    void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
    void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
    string s,t;
    int n,m,nxt[N],ans,j;
    signed main()
    {
    #ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    while(1)
    {
    //memset(nxt,0,sizeof(nxt));
    cin>>s;
    n=s.size();
    if(s=="#"&&n==1) return 0;
    cin>>t;
    m=t.size();
    s=" "+s,t=" "+t;
    j=0;
    for(int i=2;i<=m;i++)
    {
    while(j&&t[j+1]!=t[i]) j=nxt[j];
    if(t[i]==t[j+1]) j++;
    nxt[i]=j;
    }
    j=0,ans=0;
    for(int i=1;i<=n;i++)
    {
    while(j&&t[j+1]!=s[i]) j=nxt[j];
    if(t[j+1]==s[i]) j++;
    if(j==m) ans++,j=0;
    }
    write(ans);
    puts("");
    }
    }
  • 教训:

    关于此题有一个深痛教训,对于 next 数组,即使多测,每一次也都会重新处理每个 next 的值,不必清空,而由于我多次 memset 导致常数过大多次超时。

    所以:kmp 题目中,不必对 next 数组 memset

    image

总结

当时课件讲 kmp 时,那个直播的学长讲的实在难平,根本不知道在说什么,所以利用其他网站和各种途径去学。写完 oj 上少有的几道 kmp 后,这里面甚至有好几道是用哈希水过的,所以感觉掌握实在不扎实,就去 loj 上刷了一些,感觉差不多真正理解了,于是决定写一篇博客加深一下理解,防止只会搞板子,要知道板子是怎么来的。在写博客的过程中也是思考了一段时间,才搞明白到底为什么这么写,比如动物园这道题,打完一直感觉有几点是错的不知为何能过,写完博客后终于是说服了自己。next 数组的处理过程值最不容易理解的,在打这一部分的时候也是费解了好久的,发现课件讲得实在不明白后去自己理解,上网上找动图。同时上面的图除了那个动图其他基本都是自己画的,比如周期那两道,用图来理解非常的好。kmp 的做法还有很多,不能局限于匹配,在处理 next 过程中。可以处理处很多别的东西,同时在查询过程中也是可以修改 next 的,用于减少时间复杂度,仔细看周期那题的代码可以发现。最重要的,熟练掌握 nextborder 的各种含义与应用。

posted @   卡布叻_周深  阅读(45)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示