前言—— \(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\) 长度 :

      • 代码如下:

        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\) 。

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

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

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

      但使用 \(kmp\) 遍不用这样。

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

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

      而不必从头开始。

      那么这道题要求出现的次数,那么每次 \(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)\)

    看一下课件吧:

例题

\(OKR-Periods of Words\)

  • 题目链接

  • 题面:

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

  • 解法:

    先来看一张图:

    也就完美的解释了这道题,这样的话就不断跳 \(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]\) 即可,似乎更简单一点,但我们仍应该证明一下。

    其实也就是这道题: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\) 。

总结

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

KMP 学习笔记的更多相关文章

  1. KMP学习笔记

    功能 字符串T,长度为n. 模板串P,长度为m.在字符串T中找到匹配点i,使得从i开始T[i]=P[0], T[i+1]=P[1], . . . , T[i+m-1]=P[m-1] KMP算法先用O( ...

  2. 扩展kmp学习笔记

    kmp没写过,扩展kmp没学过可还行. 两个愿望,一次满足 (该博客仅用于防止自己忘记,不保证初学者能看懂我在瞎bb什么qwq) 用途 对于串\(s1,s2\),可以求出\(s2\)与\(s1\)的每 ...

  3. 扩展kmp 学习笔记

    学习了一下这个较为冷门的知识,由于从日报开始看起,还是比较绕的-- 首先定义 \(Z\) 函数表示后缀 \(i\) 与整个串的 \(lcp\) 长度 一个比较好的理解于实现方式是类似于 \(manac ...

  4. 串的应用与kmp算法讲解--学习笔记

    串的应用与kmp算法讲解 1. 写作目的 平时学习总结的学习笔记,方便自己理解加深印象.同时希望可以帮到正在学习这方面知识的同学,可以相互学习.新手上路请多关照,如果问题还请不吝赐教. 2. 串的逻辑 ...

  5. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  6. 牛客网《BAT面试算法精品课》学习笔记

    目录 牛客网<BAT面试算法精品课>学习笔记 牛客网<BAT面试算法精品课>笔记一:排序 牛客网<BAT面试算法精品课>笔记二:字符串 牛客网<BAT面试算法 ...

  7. AC自动机板子题/AC自动机学习笔记!

    想知道484每个萌新oier在最初知道AC自动机的时候都会理解为自动AC稽什么的,,,反正我记得我当初刚知道这个东西的时候,我以为是什么神仙东西,,,(好趴虽然确实是个对菜菜灵巧比较难理解的神仙知识点 ...

  8. OI知识点|NOIP考点|省选考点|教程与学习笔记合集

    点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...

  9. Hash学习笔记

    啊啊啊啊,这篇博客估计是我最早的边写边学的博客了,先忌一忌. 本文章借鉴与一本通提高篇,但因为是个人的学习笔记,因此写上原创. 目录 谁TM边写边学还写这玩意? 后面又加了 Hash Hash表 更多 ...

  10. 【学习笔记】字符串—马拉车(Manacher)

    [学习笔记]字符串-马拉车(Manacher) 一:[前言] 马拉车用于求解连续回文子串问题,效率极高. 其核心思想与 \(kmp\) 类似:继承. --引自 \(yyx\) 学姐 二:[算法原理] ...

随机推荐

  1. vue学习笔记 十八、父子组件相互传递参数

    系列导航 vue学习笔记 一.环境搭建 vue学习笔记 二.环境搭建+项目创建 vue学习笔记 三.文件和目录结构 vue学习笔记 四.定义组件(组件基本结构) vue学习笔记 五.创建子组件实例 v ...

  2. oppo和海康嵌入式软件工程师面经总结

    目录 海康 一面(3.23,35min) 自我介绍 项目介绍 你做的这个项目遇到了那些问题,如何解决的? 移植uboot,只做了移植吗? 用的那个文件系统? 移植过程中,网卡驱动做了那些工作? 写过那 ...

  3. 何时使用Kafka而不是RabbitMQ

    Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理.Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求.本文将比较 ...

  4. http连接池配置及spring boot restTemplate配置http连接池

    本文为博主原创,转载请注明出处: 项目中存在第三方系统之间的服务调用通信,且会进行频繁调用,由于很早之前实现的调用方式为每调用一次外部接口,就需要新建一个HttpClient 对象.由于频繁调用,会存 ...

  5. python json.loads()、json.dumps()和json.dump()、json.load()区别

    json.loads().json.dumps()和json.dump().json.load()分别是两组不同用法 带s的用于数据类型的转换,不带s的用于操作文件. json.loads().jso ...

  6. MCU芯片设计流程

    MCU设计流程 1.产品开发整体流程 Integrated Product Development(IPD) TR-Technique Review-技术评审 xDCP-管理层决定是否开发 这里的验证 ...

  7. 结构体Struct、联合体Union与类Class

    结构体Struct.联合体Union与类Class 1. Struct/Class struct能包含成员函数吗? 能! struct能继承吗? 能!! struct能实现多态吗? 能!!! 1.1 ...

  8. [转帖]harbor 更改网段(docker-compose)

    https://blog.csdn.net/Darkernote/article/details/119390862 问题:harbor 安装后网段冲突 docker-compose 一般安装会创建一 ...

  9. [转帖]台积电3nm成功量产,稳了吗?

    https://docs.pingcode.com/info/13836.html?p=13836 2023-01-19 资讯 21 原标题:台积电3纳米成功量产:未来与三星仍将决战鳍式场效晶体管(F ...

  10. rel分支合并进入dev分支有冲突怎么处理?

    rel分支合并进入dev分支有冲突怎么处理? 切换到本地rel 拉取远端rel 切换本地dev 拉去远端dev git merge rel 会出现冲突 解决后 推送到远端就可以