前言—— \(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. CF:706B. Interesting drink (二分查找)

    题意:不同奶茶店里同样的奶茶价格不同,问在当天Yuki持有的零钱能在几家店购买 思路:对价格数组排序,先优先判断是否会比较最大值和最小值,然后二分查找 #include<bits/stdc++. ...

  2. 【Vue CLI】手把手教你撸插件

    本文首发于 vivo互联网技术 微信公众号链接:https://mp.weixin.qq.com/s/Rl8XLUX7isjXNUmbw0-wow作者:ZhuPing 现如今 Vue 作为主流的前端框 ...

  3. java调用本机的命令 如ping、打开文本等

    最近接触到用java代码调用主机的命令部分感觉有点意思整理总结一下 环境jdk1.8  操作系统win10,不用引入其他的包jdk自带的api就可以 一.java调用ping命令 import jav ...

  4. vue 状态管理 三、Mutations和Getters用法

    系列导航 vue 状态管理 一.状态管理概念和基本结构 vue 状态管理 二.状态管理的基本使用 vue 状态管理 三.Mutations和Getters用法 vue 状态管理 四.Action用法 ...

  5. 机器学习-决策树系列-GBDT算法-集成学习-30

    目录 1. 复习 2. GBDT 3. gbdt应用于二分类: 3. gbdt应用于多类 4. 叶子节点输出值c的计算 5. GBDT的其他应用 6. GBDT+LR 代码实现 1. 复习 再开始学习 ...

  6. 搞了个Blazor工具站,域名一次性买了10年!

    大家好,我是沙漠尽头的狼. 在 Dotnet9 上线在线小工具和小游戏后,服务器的压力感觉挺大的,打开25个页面,内存占用170MB左右,CPU保持在60~70%,看来Server真不适合搞这类交互较 ...

  7. 【ThreadX-GUIX】Azure RTOS GUIX和Azure RTOS GUIX Studio概述

    Azure GUIX嵌入式GUI是Microsoft的高级工业级GUI解决方案,专门针对深度嵌入式,实时和IoT应用程序而设计.Microsoft还提供了名为Azure RTOS GUIX Studi ...

  8. 0xGame 2023【WEEK1】Crypto全解

    What's CBC? 题目信息 from Crypto.Util.number import * from secret import flag,key def bytes_xor(a,b): a, ...

  9. [转帖]decimal and numeric (Transact-SQL)

    https://learn.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql?view=sql-ser ...

  10. [转帖]Ceph简单搭建

    https://cloud.tencent.com/developer/article/1643322 Ceph基础介绍 ​ Ceph是一个可靠地.自动重均衡.自动恢复的分布式存储系统,根据场景划分可 ...