KMP 学习笔记

前言—— \(char\)\(string\)

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

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

    • \(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\) 的性质 )

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

      \(eg:\) \(abababab\)\(ab,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])\) 一定是 \(s\)\(border\)\(n\) 表示 \(s\) 的长度 )。

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

  • 基本求法:

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

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

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

      先定义一个 \(i,j\) ,先用 \(s_{j+1}\) 区匹配 \(s_i\)\(i\)\(2\) 开始, \(j\)\(0\) 开始。因为 \(next[1]\) 显然 \(=0\)

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

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

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

      打个比方,如 \(aabaaf\)

      1. \(a\) ,显然 \(next[1]=0\)
      2. \(aa\)\(s_{0+1}=s_2,j=1,next[2]=1\)
      3. \(aab\)\(s_{1+1}\neq s_3\),不断往前跳 \(j=next[j]\) ,始终不存在 \(s_{j+1}=s_3\) ,故 \(next[3]=0\)
      4. \(aaba\) ,现在经历过上一步的跳 \(next\) 使 \(j=0\)\(s_{0+1}=s_4\) ,故 \(j=1,next[4]=1\)
      5. \(aabaa\)\(s_{1+1}=s_5,j=2,next[5]=2\)
      6. \(aabaaf\)\(s_{2+1}\neq s_6\) ,不断向前跳 \(j=next[j]\) ,和第三次操作一样,始终不满足 \(s_{j+1}=s_6\) ,故 \(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\) ,当匹配 \(s_i\)\(t_{j+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. 子串周期循环问题。

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

  • 关于复杂度

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

    看一下课件吧:

    image

例题

\(OKR-Periods of Words\)

  • 题目链接

  • 题面:

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

  • 解法:

    先来看一张图:

    image

    也就完美的解释了这道题,这样的话就不断跳 \(next[i]\) ,使得到 \(>0\) 的最小的一个 \(next\) 设其为 \(j\)\(ans+=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+=i-next[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\) ,求其每一个前缀的长度 \(<\dfrac{len}{2}\)\(border\) 的个数。( \(len\) 指该前缀的长度 )

  • 解法:

    在此处换一种想法,不一定非要求自身的个数,对于一个 \(s_i\) ,我们求其后面可能出现的 \(s_j\)\(num\) ,此处 \(s_j\) 可以通过跳 \(next\) 跳到 \(s_i\) 的位置,且 \(i\) 为其跳 \(next\) 过程中第一个 \(<\dfrac{j}{2}\) 的位置。

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

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

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

    那么思考上面的情景,每一个 \(s_i\) 他的 \(num_i\) 就是他不断往前跳 \(next\) 跳多少次到 \(0\) 。又发现 \(num_i=num_{next[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\) 的,用于减少时间复杂度,仔细看周期那题的代码可以发现。最重要的,熟练掌握 \(next\)\(border\) 的各种含义与应用。

posted @ 2024-01-30 11:01  卡布叻_周深  阅读(18)  评论(3编辑  收藏  举报