KMP——从入门到不会打题


前言

  如果你不了解哈希,建议先观看本蒟蒻的另一篇博客,对哈希有一定的理解   哈希大法吼

  KMP算法,别名烤馍片或者看毛片,由烤馍片男子天团三位神犇同时发现的一种强大的单模式串匹配算法

  通俗翻译即寻找一个模式串是否在一个文本串中出现过,出现过几次,出现的位置等等。

  用于更快速地将口吐芬芳的用户禁言


一般算法解决问题

  首先我们分析一般的单模式串匹配算法:

  1.暴力枚举法:

    每次依次匹配两个字符串的每一位,这样如果是aaaaaaa....这种字符串的话复杂度就会高达O(n*m);

  2.哈希优化法:

    预处理出模式串和文本串的哈希,枚举一个起点用哈希O(1)判断是否相等,时间复杂度O(文本串);

    好像挺快的啊,那还学什么KMP啊,好了本文到此结束。

  既然哈希优化暴力枚举即可达到O(文本串)的优秀复杂度我,为何还要学习复杂度为O(文本串+模式串)的KMP算法呢?这与KMP的实现原理有关


KMP算法

  KMP思想

  仔细观察暴力枚举的漏洞,如果一个模式串在当前位置匹配完毕(无论失败成功),模式串首部只能前进一位,而中间可能包含大量无用匹配,这些匹配是之前已经确认失败的。

  而KMP思想的先进之处在于,每次模式串在当前位置匹配完毕之后,充分利用之前信息,将已经匹配成功的部分跳过从而减少时间复杂度

  举个栗子:

  文本串:abcabqwq

  模式串:abcabe

  在这两种模式串匹配过程中,显然模式串与文本串在匹配到第六位的时候会失配,在一般的算法中下一步会变成酱紫

  文本串:abcabqwq

  模式串:  abcabe

  然而我们观察到,如果文本串bcab...能够和模式串abcabe匹配上,那么模式串的e之前的四位字符应该与开头的四位字符相同才对,毕竟e之前的四位已经匹配成功了,但显然是不同的。

  继而我们得出一个结论:如果一个字符串移动后能够匹配上,那么已经匹配成功的子串的后几位字符应该与开头的几位字符相同。

  在模式串abcabe中,匹配成功的子串abcabc中只有ab与开头部分相同,那么我们大可以直接将ab移动到相应的位置上去:

  文本串:abcabqwq

  模式串:      abcabe

  如果移动以后仍然不能匹配成功呢?那么只要重复这个过程,直到下一位可以匹配上,或者模式串已经不能通过这种方式移动了,再向右继续匹配。

  为了不遗漏匹配,我们必须保证所有可能匹配成功的地方都扫过一遍,也就是说,我们模式串向右移动的距离应该尽可能短,这是为了保证正确性。

  所以我们要找模式串中每个前缀的最大后缀等于前缀(注意不能相等哦!),qwq有点拗口希望仔细理解一下,然后开一个数组(我们称为失配数组)记录这个后缀的长度,方便每次跳串(跳串什么鬼啦

  下文中我们称这个最大后缀等于前缀中的后缀为失配后缀,前缀为失配前缀,失配数组为nxt,文本串为s1,模式串为s2

  模式串前缀:a ab abc abca abcab abcabe

  失配数组:   0 0 0 1 2 0

  失配数组获取:

    我们设置两个指针,i 负责扫描整个模式串,t 负责指向当前失配前缀。

    初始时刻 t 应滞后 i 一个位置,毕竟不能相等嘛qwq

    然后我们将要进行的是一个模式串自己匹配自己的过程,将 i 与 t 向右推进,每次失配时t2直接利用已经处理出来的失配数组向回跳

  栗子:

    模式串:a b a c a b

       t   i                               初始状态,nxt [ 1 ] = 0; 然而 s2 [ i+1 ]与s2 [ t+1 ]不匹配,且t无法向前跳,所以t不移动,++i;

    模式串:a b a c a b

         t       i                            此时nxt [ 2 ] = 0;,发现 s2 [ i+1 ]与s2 [ t+1 ] 匹配,++t;,++i;

    模式串:a b a c a b

           t     i        nxt [ 3 ] =1,下一步s2 [ i+1 ]与s2 [ t+1 ]不匹配,t沿着nxt [ t ]数组向回跳,直到下一位匹配或者无法继续跳。

    实现方法有两种,总的来说第二种性质更优秀,但第一种更容易理解(个人认为),在这里两种方法都展示出来

    

  1. inline void get_nxt()//第一种
  2. {
  3. int t1=,t2;
  4. nxt[]=t2=-;
  5. while(t1<len2)
  6. {
  7. if(t2==-||s2[t1]==s2[t2])
  8. nxt[++t1]=++t2;
  9. else
  10. t2=nxt[t2];
  11. }
  12. }
  13.  
  14. inline void get_nxt()//第二种
  15. {
  16. t=;
  17. for(int i=;i<len2;++i)
  18. {
  19. while(t&&s2[t]!=s2[i]) t=nxt[t];
  20. t+=(s2[t]==s2[i]);
  21. nxt[i+]=t;
  22. }
  23. }
  24.  
  25. inline void get_nxt()//第二种下标从1开始的写法
  26. {
  27. t=;
  28. for(int i=;i<=len2;++i)
  29. {
  30. while(t&&s2[t+]!=s2[i]) t=nxt[t];
  31. t+=(s2[t+]==s2[i]);
  32. nxt[i]=t;
  33. }
  34. }

  匹配时更加简单,匹配成功就前进一位,失败就跳失配数组

  1. inline void kmp()//第一种 对应
  2. {
  3. int t1=,t2=;
  4. while(t1<len1)
  5. {
  6. if(t2==-||s1[t1]==s2[t2])
  7. t1++,t2++;
  8. else t2=nxt[t2];
  9. if(t2==len2)
  10. {
  11. //匹配成功,其他操作
  12. t2=nxt[t2];
  13. }
  14. }
  15. }
  16. inline void kmp()//第二种 对应
  17. {
  18. t=;
  19. for(int i=;i<len1;++i)
  20. {
  21. while(t&&s2[t+]!=s1[i]) t=nxt[t];
  22. t+=(s2[t+]==s1[i]);
  23. if(t==len2-)
  24. {
  25. //匹配成功,其他操作
  26. }
  27. }
  28. }
  29. inline void kmp()//第二种 下标 1 开始
  30. {
  31. t=;
  32. for(int i=;i<=len1;++i)
  33. {
  34. while(t&&s2[t+]!=s1[i]) t=nxt[t];
  35. t+=(s2[t+]==s1[i]);
  36. if(t==len2)
  37. {
  38. //匹配成功,原地爆炸
  39. t=nxt[t];
  40. }
  41. }
  42. }

  看到这里相信各位有所感触:KMP算法精华在于nxt数组,它在O(文本串+模式串)的时间内还求出了模式串每个前缀的 最大后缀等于前缀 这一重要信息,相比之下,匹配文本倒是其次(毕竟单纯的匹配的话哈希爆搞也能搞过),该算法一些优秀的扩展大部分也是基于nxt数组的性质来实现的。


KMP应用

  1.最小后缀等于前缀

  洛谷P3435

  给定长度为n的字符串,求字符串每个前缀的最小后缀等于前缀。

  分析:这里设计到一个性质:失配前缀的后缀仍是失配后缀的一部分(毕竟失配前缀完全等同于失配后缀嘛)

  由于这条性质,我们只要一直沿着nxt数组向前跳,直到nxt [ t ] = 0,考虑到复杂度问题,我们在路径上加一个类似并查集的路径压缩操作;

  2.最短循环节

  POJ2406

  求一个字符串最多由几个循环节拼成

  分析:还记得我们的神仙结论吗?判断一个字符串[ l , r ]是否存在一个长度为d的循环节,只需判断 [ l+d , r ] 和 [ l , r-d ] 是否相等。 

  再联想nxt数组的内容:最大后缀等于前缀的长度。

  答案就呼之欲出了!

  最小循环节长度 res = len - nxt [ len ] ;

  最多循环节数量 ans = len / res ;

  这个神仙结论证明有点子麻烦我不想在这里写emmmmm,大家脑补一下好了。

  3.循环节的判断与修改

  HDU3746

  最少添加多少字符能获得循环节

  分析:首先若补充或补充最少,找到最小循环节一定是最优的,这里利用上面的最小循环节姿势。

  分类讨论: 若nxt [ len ] 等于0,则需要补充 len 个字符

  若nxt [ len ]不为0  若存在循环节,即len%(len - nxt [ len ])== 0 ,不需要补充字符

  不存在循环节,那么至少用补充(len - nxt [ len ])- (len % (len - nxt [ len ]))(最小循环节长度减去已经存在而不足的长度)

  51nod1554

  给定字符串,求有多少个前缀满足A+B+A+B+……+A,由k+1个A和k个B构成,其中A和B可以为空串。

  分析:

  我们可以将某一前缀串(后面称为当前串)分为两种类型:

  1.SSSSSSS类型:(S为最短循环节)

    我们可以求得S的个数为 num = len / (len - nxt [ len ]);

    那么既然要求以A结尾,最后的烂摊子必然是A处理,所以A就应该等于 num % k个S;

    那么完整的A+B个数就是 num / k 个;

    如果当前串满足A+B+A+B+……+A;那么A应该至少可以在A+B中占有一席之地,也就是说A的长度要小于等于A+B,剩下的作为B即可(B可以是空串,所以包括等于);

    即 num / k >= num % k;

  2.SSSSSSST类型:

    A继续收尾,所以A必然等于T , B只能等于 (S - T) + 数个S

    完整A+B个数仍然是num / k,T的个数是 num % k;

    由于这次 T ! = S , 所以成立条件变为 num / k > num % k;

  4.KMP优化DP

  CF1163D

  给定一个字符串c(0<=|c|<=10000<=∣c∣<=1000),c由∗和小写字母组成,你可以给∗填上任何一个小写字母

  再给定两个字符串s,t,(0<=|s|,|t|<=50)(0<=∣s∣,∣t∣<=50),求如何填c中的*∗,使得c中包含s的次数 - c中包含t的次数最大

  分析:先咕咕咕,不太理解。(失配树锅已补,这个再咕咕两天)

  5.失配树(我自己起的名字qwq)

  洛谷P3426

  给定一个字符串,求可重叠的最小循环节。

  分析:(该题目分析大多摘自题解,加上了自己的一些理解,解释起来更通俗(至少我是这么认为的)

  可重叠最小循环节必定具有如下性质:

  1.是原字符串的前后缀

  2.假设有两个前缀 s 和 t 如果s<t(长度)且t是一个可重叠循环节,那么s也是一个可重叠循环节

  3.两个可重叠循环节起点之间距离不超过循环节长度

  对于性质1:只要跑一遍KMP,然后从nxt [ len ]一直往前跳,把路上的长度全部扔进栈,候选集合就完成了;

  对于性质2:从小到大枚举判断答案;

  对于性质3:如果从小到大枚举判断,把比当前循环节小的循环节中的节点全部删去,如果最大空隙小于当前循环节长度,那么该循环节就是答案;

     如何实现在枚举到一个循环节的时候删去所有比他小的循环节中的节点?

  我们可以考虑建一颗fail树(失配树),建立方式如下:

  

  1. for(int i=;i<=len;++i) add(nxt[i],i);

  这时每个节点既代表循环节长度,又代表了在原串中的位置

  由于性质1,所有答案一定在0到n这一条路径上;

  对于每个节点 x 满足以它为前缀和后缀的节点一定是它的子树;

  我们只要利用一个双向链表,每次删去非 x 子树的节点,然后更新最大空隙。

  当最大空隙小于当前节点(循环节长度时),该节点的值就是答案

  

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define int long long
  4. inline int read()
  5. {
  6. int x=,f=;
  7. char ch;
  8. for(ch=getchar();(ch<''||ch>'')&&ch!='-';ch=getchar());
  9. if(ch=='-') f=,ch=getchar();
  10. while(ch>=''&&ch<=''){x=(x<<)+(x<<)+ch-'';ch=getchar();}
  11. return f?x:-x;
  12. }
  13. string s;
  14. int len,res;
  15. int nxt[];
  16. int st[],top;
  17. int pre[],to[];
  18. struct point
  19. {
  20. int nxt,to;
  21. }a[];
  22. int head[],cnt;
  23. inline void add(int x,int y)
  24. {
  25. a[++cnt].nxt=head[x];
  26. a[cnt].to=y;
  27. head[x]=cnt;
  28. }
  29. inline void get_nxt()
  30. {
  31. int t=;
  32. for(int i=;i<len;++i)
  33. {
  34. while(t&&s[t]!=s[i]) t=nxt[t];
  35. t+=(s[t]==s[i]);
  36. nxt[i+]=t;
  37. }
  38.  
  39. for(int i=;i<=len;++i) add(nxt[i],i);
  40. }
  41. inline void del(int x)
  42. {
  43. to[pre[x]]=to[x];
  44. pre[to[x]]=pre[x];
  45. res=max(res,to[x]-pre[x]);
  46. to[x]=pre[x]=;
  47. }
  48. inline void bfs(int x,int y)
  49. {
  50. queue<int> q;
  51. q.push(x);
  52. while(!q.empty())
  53. {
  54. int now=q.front();
  55. q.pop();
  56. if(now==y) continue;
  57. del(now);
  58. for(int i=head[now];i;i=a[i].nxt)
  59. {
  60. int t=a[i].to;
  61. q.push(t);
  62. }
  63. }
  64. }
  65. signed main()
  66. {
  67. cin>>s;
  68. len=s.length();
  69. get_nxt();
  70. for(int i=len;i;i=nxt[i]) st[++top]=i,st[top+]=;
  71. for(int i=;i<=len;++i) pre[i]=i-,to[i]=i+;
  72. res=;
  73. for(int i=top;i;--i)
  74. {
  75. bfs(st[i+],st[i]);
  76. if(res<=st[i])
  77. {
  78. printf("%lld\n",st[i]);
  79. break;
  80. }
  81. }
  82. return ;
  83. }

KMP——从入门到不会打题的更多相关文章

  1. 【面向打野编程】——KMP算法入门

    一.问题 咱们先不管什么KMP,来看看怎么匹配两个字符串. 问题:给定两个字符串,求第二个字符串是否包含于第一个字符串中. 为了具体化,我们以 ABCAXABCABCABX 与 ABCABCABX为例 ...

  2. KMP算法入门

    学一把看毛片算法我觉得自己才能变得更加出色 明明昨天的题我都知道怎么模拟了,但是还是不会改KMP,是我学丑了 KMP是Knuth-Morris-Pratt三人设计的线性时间字符串匹配算法 nxt数组的 ...

  3. Webpack 入门指迷--转载(题叶)

    最近看到这个东西,一头雾水.看了一些资料了解了Webpack概念,大体是webpack 是一个模块绑定器,主要目的是在浏览器上绑定 JavaScript 文件. 看到题叶写的一篇介绍,写的很好,转载连 ...

  4. 【初识】KMP算法入门(转)

    感觉写的很好,尤其是底下的公式,易懂,链接:http://www.cnblogs.com/mypride/p/4950245.html 举个例子 模式串S:a s d a s d a s d f a  ...

  5. POJ 2185 Milking Grid (KMP,求最小覆盖子矩阵,好题)

    题意:给出一个大矩阵,求最小覆盖矩阵,大矩阵可由这个小矩阵拼成.(就如同拼磁砖,允许最后有残缺) 正确解法的参考链接:http://poj.org/showmessage?message_id=153 ...

  6. 【初识】KMP算法入门

    举个例子 模式串S:a s d a s d a s d f a s d 匹配串T:a s d a s d f 如果使用朴素匹配算法—— 1 2 3 4 5 6  8 9 a s d a s d a s ...

  7. 入门训练 Fibonacci数列 (水题)

    入门训练 Fibonacci数列   时间限制:1.0s   内存限制:256.0MB        问题描述 Fibonacci数列的递推公式为:Fn=Fn-1+Fn-2,其中F1=F2=1. 当n ...

  8. KMP算法入门讲解

    字符串匹配问题.假设文本是一个长度为$n$的字符串$T$,模板是一个长度为$m$的字符串$P$,且$m\leq n$.需要求出模板在文本中的所有匹配点$i$,即满足$T[i]=P[0],T[I+1]= ...

  9. Python 从入门到入门基础练习十五题

    **a) 6.成绩转换:编写一个学生成绩转换程序,用户输入百分制的学生成绩,成绩大于或等于60的输出"pass",否则输出"fail",成绩不四舍五入. a = ...

随机推荐

  1. ​为什么我会选择走 Java 这条路?

    ​本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点 ...

  2. ng 判定输入的手机号是否正确

    判定输入的手机号是否正确   infoConfirm(){        if (!/^1[3456789]\d{9}$/.test(this.mobile)) {          this.pho ...

  3. c++实现文件复制并修改相应属性

    问题描述 完成一个目录复制命令mycp,包括目录下的文件和子目录, 运行结果如下: beta@bugs.com [~/]# ls –la sem total 56 drwxr-xr-x 3 beta ...

  4. Java编程基础——数组和二维数组

    Java编程基础——数组和二维数组 摘要:本文主要对数组和二维数组进行简要介绍. 数组 定义 数组可以理解成保存一组数的容器,而变量可以理解为保存一个数的容器. 数组是一种引用类型,用于保存一组相同类 ...

  5. MQ选型对比ActiveMQ,RabbitMQ,RocketMQ,Kafka 消息队列框架选哪个?

    最近研究消息队列,发现好几个框架,搜罗一下进行对比,说一下选型说明: 1)中小型软件公司,建议选RabbitMQ.一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便.不考虑r ...

  6. HTTP Protocol

    HTTP协议 1      HTTP请求状态码 当用户试图通过 HTTP 访问一台正在运行 Internet 信息服务 (IIS) 的服务器上的内容时,IIS 返回一个表示该请求的状态的数字代码.状态 ...

  7. Spring框架完全掌握(上)

    引言 前面我写了一篇关于Spring的快速入门,旨在帮助大家能够快速地了解和使用Spring.既然是快速入门,讲解的肯定只是一些比较泛的知识,那么对于Spring的一些深入内容,我决定将其分为上.下两 ...

  8. Scrum 冲刺第四篇

    我们是这次稳了队,队员分别是温治乾.莫少政.黄思扬.余泽端.江海灵 一.会议 1.1  28号站立式会议照片: 1.2  昨天已完成的事情 团队成员 昨日已完成的任务 黄思扬 活动内容管理页(前端) ...

  9. RPC 初识

    RPC是什么 RPC(Remote Procedure Call) 释义是远程过程调用,常存在于分布式系统中. 比如说现在有两台服务器A, B,一个在A服务器上的应用想要调用B服务器上的应用提供的某个 ...

  10. Django 练习班级管理系统一

    创建项目 user_manager 和 app为 app01 models.py 为 from django.db import models # Create your models here. c ...