首页 > 其他分享 >KMP 学习笔记

KMP 学习笔记

时间:2024-01-30 11:12:58浏览次数:23  
标签:pre 匹配 int kmp 笔记 next 学习 KMP border

前言—— \(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\) 的各种含义与应用。

标签:pre,匹配,int,kmp,笔记,next,学习,KMP,border
From: https://www.cnblogs.com/Charlieljk/p/17996695

相关文章

  • 【学习笔记】 - 可持久化数据结构初步:可持久化线段树
    前置知识:权值线段树权值线段树每个节点不再是区间,而是值在某个范围内的个数可以用于求区间第\(k\)大值动态开点一个点只有在需要时才被创建正文什么是可持久化数据结构就是说这个数据结构能保留每一个历史版本且支持操作可持久化线段树又称函数式线段树/主席树......
  • 云计算学习day6
    首先学习了如何建立yum源仓库1.mount/dev/sr0/mnt(将光驱sr0挂载到mnt文件夹)(挂载:当linux操作系统需要使用外来硬件时,需要将硬件进行挂载,把Linux当中的文件夹和硬件做上关联)2.cd/etc/yum.repos.d/(切换到配置文件下)客户端的配置文件必须在规定路径下(/yum.etc/repos.d/)配......
  • OpenGL学习(一)
    OpenGL学习(一)基本概念因为OpenGLES是OpenGL的一个子集,所以下面就主要介绍一些有关OpenGL的一些基本概念。OpenGL的结构可以从逻辑上划分为下面3个部分:图元(Primitives)缓冲区(Buffers)光栅化(Rasterize)图元(Primitives)在OpenGL的世界里,我们只能画点、线、三角形这......
  • OpenGL学习(二)——GLFW
    OpenGL学习(二)——GLFW参考资料:【双语】【TheCherno】OpenGL_哔哩哔哩_bilibili[LearnOpenGLCN(learnopengl-cn.github.io)](https://learnopengl-cn.github.io/01Gettingstarted/02Creatingawindow/)LearnOpenGL示例环境搭建-知乎(zhihu.com)创建窗口[你好,窗......
  • OpenGL学习(三)——GLSL
    OpenGL学习(三)——GLSL参考资料:【双语】【TheCherno】OpenGL_哔哩哔哩_bilibili[LearnOpenGLCN(learnopengl-cn.github.io)](https://learnopengl-cn.github.io/01Gettingstarted/02Creatingawindow/)LearnOpenGL示例环境搭建-知乎(zhihu.com)GLSL着色器(Shad......
  • Power BI - 5分钟学习创建合并列
    每天5分钟,今天介绍PowerBI如何创建合并列什么是合并列顾名思义合并列就是把两个列信息拼接到一个列中显示。工作中经常会有类似需求,把产品编码和产品名称放到一个筛选器或者单元格中展示。那我们在PowerBI中应该如何进行类似创建合并列的操作呢?首先导入样例产品表;(Excel数据......
  • 非内积级联学习
    1.首页推荐非内积召回现状非内积召回源是目前首页推荐最重要的召回源之一。同时非内积相比于向量化召回最终仅将user和item匹配程度表征为embeding内积,非内积召回仅保留itemembedding,不构造user显式表征,而是通过一个打分网络计算用户-商品匹配程度,极大的提升了模型精准度的上限,......
  • WebAssembly入门笔记[4]:利用Global传递全局变量
    利用WebAssembly的导入导出功能可以灵活地实现宿主JavaScript程序与加载的单个wasm模块之间的交互,那么如何在宿主程序与多个wasm之间传递和共享数据呢?这就需要使用到Global这个重要的对象了。一、数值类型全局变量二、将JavaScript函数设置为全局变量三、利用全局变量处理字符......
  • 《梦断代码》阅读笔记2
    当今社会,软件已经成为人类生活中不可或缺的一部分,“人类文明运行于软件之上”的说法虽然有点自卖自夸,但它很是明确的反应了软件在人类社会中的地位。它存在于厨具里、汽车里、玩具里、建筑中,商业、科研、医疗、基础公共设施哪里都有它的影子,人类生存之所需都系于计算机代码这根易......
  • 《梦断代码》阅读笔记3
    寒假静下来读书的时间比较少,因此我并没有读完《梦断代码》这本有意思的书,以后会慢慢读的,现在说一说目前读完的部分的感受吧。首先,这本书深入讨论了软件开发的复杂性和编程的挑战性,尤其是在项目管理和时间规划方面。对于“软件时间”的分析让我意识到在实际编程中,时间管理并非总是......