(一)如何优化暴力算法

Waiting...

(二)KMP模板

 KMP 算法的精髓在于 \(next\) 数组,\(next[i]=j\) 代表 \(p[1,j]=p[i-j+1,i]\),\(next[i]\) 数值意义代表 \(p[1,i]\) 所有前缀与后缀的最长公共部分,我们约定本文提到的前缀与后缀均为不包含原字符串 \(p\) 本身。

 这个过程动手计算并不困难,但想理解代码为何如此实现倒并不简单。个人建议,自行动手画图计算 \(next\) 数组,以此体会这个过程,否则永远不可能理解哪怕一行代码。由于 \(next\) 可能会有命名与内置命名冲突的风险,因而我们的代码实现中写为 \(nxt\) 而非 \(next\),下文我们都将使用 \(nxt\),同时为使代码实现起来更加简洁,我们所有字符串的索引均从 \(1\) 开始计算。

 说完约定之后,接着来看模式串 \(p\) 的最长前缀与后缀公共长度的计算过程,前缀字符最大可取到的范围是在 \(s[1,n-2]\),后缀最大可取到的范围是在 \(s[2,n-1]\),例如 \(abcd\) 前缀包括\(\{a、ab、abc\}\),后缀包括了\(\{d、cd、bcd\}\),值得注意,后缀的字符出场顺序与原字符串 \(p\) 是一样的,都是从左往右。回忆一下 \(nxt[i] = j\),其含义是指 \(p[1,j]=p[i-j+1,i]\),注意,此处使用方括号与逗号分隔的表示法是指左闭右闭区间,使用方括号与冒号分隔的切片表示法才是左闭右开区间。我们计算 \(nxt\) 数组的过程其实就是字符串 \(p\) 对其自身的匹配过程,我们错开一位再进行比较即可比对所有前缀与所有后缀之间,最大公共长度部分了。

 再看上图的匹配过程,如果失配,指向模式串的指针 \(p\) 往左退,除非已经到达了退无可退的状态,方才跳出。在完成了向左移动退步的操作之后,再对当前文本串 \(t[i]\) 位置与模式串进行对比,指针 \(i\) 从始至终都只向右移动。下列模板来自 ACwing831 ,若条件允许,强烈建议购买 ACwing 算法课程,其它模板题亦有 LeetCode28洛谷3375等等。

#include<bits/stdc++.h>
using namespace std; #define MAXN 1000010 char txt[MAXN], pat[MAXN]; int nxt[MAXN];
int main(){
int n, m;
scanf("%d %s", &n, pat + 1);
scanf("%d %s", &m, txt + 1);
// 显然i = 1不符合条件,此时即既没有非平凡前缀又没有非平凡后缀
for(int i = 2, j = 0; i <= n; i++){
while(j && pat[i] != pat[j + 1])
j = nxt[j];
if(pat[i] == pat[j + 1]) j++;
nxt[i] = j;
}
for(int i = 1, j = 0; i <= m; i++){
while(j && txt[i] != pat[j + 1])
j = nxt[j];
if(txt[i] == pat[j + 1]) j++;
if(j == n){
printf("%d ", i - n);
j = nxt[j];
}
}
printf("\n");
return 0;
}

 KMP 算法的本质是利用字符串本身蕴含的冗余信息,通俗来说就是利用自身的相同部分来减少比对的次数,很直观的,当出现失配的时候,对于已经比过的、相同的部分,我们显然不需要重新再比对一次。

 当一个字符串是由某个循环节,循环若干次构成的时候,此时字符串蕴含的冗余信息几乎是最理想的状态,KMP 认为所有的字符串均是通过某个循环节,进行若干次循环之后,再截取子串获得的,例如字符串 \(cabcabca\) 其实就是通过 \(abc\) 循环四次之后所得的 文本串\(txt\) 截取 \(txt[3,10]\) 部分得到的。下文“KMP理解加深”章节中,我们会继续讨论这一点。

(三)KMP理解加深

(3.1)重复的子字符串问题

 给定一个非空的字符串 \(s\) ,检查 \(s\) 是否可以通过由其一个子串重复多次构成,假如 \(s\) 包含若干子串 \(x\),不妨记 \(s=kx\),其中 \(k>=2\),那么 \(s+s=2kx\),掐头去尾丢弃两个字符,也就是相当于破坏了头尾部分两个 \(x\) 子串,此番操作之后,剩余 \(2(k-1)x\),由于 \(k>=2\),代入可知 \(s\) 至少会在 \(2(k-1)x\) 出现一次,因而只要对于切片 \(2s[1:-1]\) 检查是否包含 \(s\) 即可知道 \(s\) 是否可以通过由其一个子串重复多次构成。

class Solution {
public:
bool repeatedSubstringPattern(string s) {
string cp = "#" + s;
string txt = "#" + s.substr(1) + s; txt.pop_back();
vector<int> nxt(cp.size() + 1, 0);
for(int i = 2, j = 0; i < cp.size(); i++){
while(j && cp[i] != cp[j + 1])
j = nxt[j];
if(cp[i] == cp[j + 1]) j++;
nxt[i] = j;
}
for(int i = 1, j = 0; i < txt.size(); i++){
while(j && txt[i] != cp[j + 1])
j = nxt[j];
if(txt[i] == cp[j + 1]) j++;
if(j == cp.size() - 1){
return true;
}
}
return false;
}
};

 这道题是比较简单的面试题,检查字符串是否可由多个重复的子串构成,本题放在此处主要是为了下一题分析字符串子串循环节长度、循环次数做铺垫。

 为了便于形式化表达,我们不妨使用记号 \(nxt^2[j]\) 代表 \(nxt[nxt[j]]\),以此类推,我们可以写出 \(nxt^k[j]\), 其中 \(k\)是整数,且不大于 \(n-1\),这种形式化的表达会在下文帮助我们理解 KMP 算法的本质。

(3.2)串周期

 给定一个字符串,其前缀是从第一个字符开始的连续若干个字符,在本例中,我们规定前缀包括字符串本身,例如 \(abaab\) 共有前缀 \(\{a,ab,aba,abaa,abaab\}\),我们希望对每个前缀\(s[1,i],(i>1)\)判断是否具有循环节,如果存在循环节,其长度是多少,循环次数是多少。

 对于某一个字符串\(s[1,i]\),在其众多的 \(nxt[1...i]\) 候选项中,如果存在于一个\(nxt[x],x\in[1...i]\)使得 \(i\%i - nxt[x] = 0\), 那么\(s[1,i-nxt[x]]\), 也即第二行图例中的灰色部分,即为 \(s[1,i]\) 循环元,其循环次数 \(K=i/(i-nxt[x])\)。

 其实,只要 \(s[1,i]\) 是由若干循环节构成的,那么我们要找的 \(nxt[x]\) 其实就是 \(nxt[i]\),因为 \(nxt[i]\) 代表着 “失配时指针向左移动最少的位置”,如果存在其它 \(nxt[x],x \neq i\) 同样满足 \(i\%i - nxt[x] = 0\),则其需要移动的位置一定多于 \(nxt[i]\),也就是说,此时的 \(i-nxt[x]\) 并非最小单元长度。

 举个例子,比如字符串 \(abababab\),最小的循环节应为 \(ab\),长度\(2\),满足\(8\%2=0\),但是 \(abab\) 长度\(4\),也能构成一个循环节,同样满足 \(8\%4=0\),但它并非最小循环节。

#include <bits/stdc++.h>
#include <algorithm>
using namespace std; char str[1000010];
int nxt[1000010]; int n;
int kase;
int main(){
while(scanf("%d", &n) != EOF){
if(n == 0) break;
scanf("%s", str + 1);
for(int i = 2, j =0; i <= n; i++){
while(j && str[i] != str[j + 1])
j = nxt[j];
if(str[i] == str[j + 1]){
j++;
}
nxt[i] = j;
}
printf("Test case #%d\n", ++kase);
for(int i = 2; i <= n; i++){
if(i % (i - nxt[i]) == 0 && nxt[i]){
printf("%d %d\n", i, i/(i - nxt[i]));
}
}
printf("\n");
}
return 0;
}

 本题能够延伸得到一些其它结论,详见下列条目,其中 \(x\) 代表的含义与上文相同:

  • 如果\(i-nxt[i]\) 能够整除 \(i\),那么\(s[1,i]\)具有最小循环节,长度 \(i-nxt[i]\)
  • 如果\(i-nxt[x]\),(\(x>0\) 且 \(x \neq i)\) 能够整除 \(i\),那么\(s[1,i]\) 具有循环元,长度 \(i-nxt[x]\)
  • 其余候选项 \(nxt[x]\) 均满足 \(x > 0, x = nxt^k[i]\),其中 \(k=1,2,3,4...n-1\)
  • 任意一个循环元的长度必然是最小循环元的整数倍
  • 如果 \(i-nxt[i]\) 无法整除 \(i\),那么任意 \(i-nxt[x]\) 均不可能作为 \(s[1,i]\) 循环元
  • 无论 \(m=i-nxt[i]\) 可否整除 \(i\),\(i-nxt[x]\) 都等于若干倍 \(m\),也即 \(i-nxt[x]=bm\)

 对于最后一条可能的会使得感到抽象,我们举个例子。使用的 \(abc\) 作为循环节反复拼接四次构成新字符串 \(abcabcabcabc\),然后截取其中一个片段 \(cabcabca\) 作为我们接下来分析的文本串\(txt\),我们先算这个片段 \(nxt\) 数组。

 显然文本串 \(txt\) 没有循环节,但是 \(8-nxt[8]=3\),所得的数值 \(3\) 竟然就是我们最初用于构造的 \(txt\) 循环节的长度,\(8-nxt^2[8]=6\),所得数值恰好是循环节的两倍,符合我们上面所说的规律,这是巧合吗?我们接着往下迭代,由于 \(nxt^3[8]=0\) 已经不在候选项中了,我们不再往下分析。其实,上述的过程其实并非巧合,相反其恰恰道出了 KMP 算法的本质,也即所有字符串均可通过循环节进行若干次循环之后截取子串得到。通常 \(i-nxt[i]\) 即可推算得出用于构造 \(s[1,i]\) 文本串的循环节长度。理解了这点,也就不难做出 AC4188连接字符串UVA10298这几道题了。

(3.3)匹配统计

Waiting...

(3.4)处理字符矩阵

Waiting...

(四)KMP算法变体

(4.1)构造Z函数求解LCP

Waiting...

(4.2)自动机模型

Waiting...

支持作者

道长的算法笔记:KMP算法及其各种变体的更多相关文章

  1. 算法笔记--KMP算法 && EXKMP算法

    1.KMP算法 这个博客写的不错:http://www.cnblogs.com/SYCstudio/p/7194315.html 模板: next数组的求解,那个循环本质就是如果相同前后缀不能加上该位 ...

  2. 算法:KMP算法

    算法:KMP排序 算法分析 KMP算法是一种快速的模式匹配算法.KMP是三位大师:D.E.Knuth.J.H.Morris和V.R.Pratt同时发现的,所以取首字母组成KMP. 少部分图片来自孤~影 ...

  3. 算法 | 串匹配算法之KMP算法及其优化

    主串 s:A B D A B C A B C 子串 t:  A B C A B 问题:在主串 s 中是否存在一段 t 的子串呢? 形如上述问题,就是串匹配类问题.[串匹配--百度百科] 串匹配问题是一 ...

  4. BF算法与KMP算法

    BF(Brute Force)算法是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符:若不相等,则比较S的 ...

  5. 数据结构之BF算法,kmp算法,三元组,十字链表总结

    在这一章中,老师教了我们四种数据结构:BF算法,kmp算法,三元组和十字链表:还给我们讲了2019年团体天体赛中T1-8的AI题 1.对于BF和kmp算法,老师除了在课堂上讲解算法的主要核心思想外,还 ...

  6. [一本通学习笔记] KMP算法

    KMP算法 对于串s[1..n],我们定义fail[i]表示以串s[1..i]的最长公共真前后缀. 我们首先考虑对于模式串p,如何计算出它的fail数组.定义fail[0]=-1. 根据“真前后缀”的 ...

  7. 学习笔记-KMP算法

    按照学习计划和TimeMachine学长的推荐,学习了一下KMP算法. 昨晚晚自习下课前粗略的看了看,发现根本理解不了高端的next数组啊有木有,不过好在在今天系统的学习了之后感觉是有很大提升的了,起 ...

  8. 串、串的模式匹配算法(子串查找)BF算法、KMP算法

    串的定长顺序存储#define MAXSTRLEN 255,//超出这个长度则超出部分被舍去,称为截断 串的模式匹配: 串的定义:0个或多个字符组成的有限序列S = 'a1a2a3…….an ' n ...

  9. 【算法】KMP算法

    简介 KMP算法由 Knuth-Morris-Pratt 三位科学家提出,可用于在一个 文本串 中寻找某 模式串 存在的位置. 本算法可以有效降低在一个 文本串 中寻找某 模式串 过程的时间复杂度.( ...

  10. KMP算法及KMP算法的应用(POJ2406)

    ///KMP算法#include<bits/stdc++.h> using namespace std; ]; void makeNext(const char P[],int next[ ...

随机推荐

  1. Docker | dockerfile构建centos镜像,以及CMD和ENTRYPOINT的区别

    构建自己的centos镜像 docker pull centos下载下来的镜像都是基础版本,缺少很多常用的命令功能,比如:ll.vim等等, 下面介绍制作一个功能较全的自己的centos镜像. 步骤 ...

  2. springboot中使用mybatisplus自带插件实现分页

    springboot中使用mybatisplus自带插件实现分页 1.导入mybatisplus分页依赖 <dependency> <groupId>com.baomidou& ...

  3. Linux系统管理_软件管理

    RPM命令 #管理.rpm文件 #RPM包软件命名规则:name-version-releases.arch.rpm rpm -ivh pkgname #安装软件包 rpm -ivh --test p ...

  4. LcdTools如何自定义读写PX01 SSD2828寄存器

    LcdToos打开相应的工程文件,连接PX01并开启点亮屏使LcdTools开关处于开启状态. 切到"测试设置"栏,在"Bridge控制"栏,在"Ad ...

  5. 【UML】统一建模语言

    如果是准备学习设计模式的同学,可以只了解类图相关的知识 而如果是在准备软件设计师考试的同学,或许会对你有点帮助 正在施工...... 参考博客:https://blog.csdn.net/unique ...

  6. SQL server 操作相关

    1.更改列的顺序后进行保存. 在SQL Server Management Studio中, "工具"--"选项"--"Designers" ...

  7. ui自动化测试数据复原遇到的坑——1、hibernate输出完整sql

    公司老项目使用SSH+informix+weblogic+IE开发,我们要做ui自动化测试,其中的测试数据复原,我打算通过hibernate输出sql,然后把插入.更新的sql改为delete或upd ...

  8. 【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「Logback-MDC篇」

    日志追踪 日志追踪对于功能问题的排查和数据流转的路径分析时非常重要的,有了全链路日志追踪体系机制可以非常有效且快速的定位问题,但在多线程环境中,若没有相关成熟的框架的支持,想要实现日志追踪,就需要手动 ...

  9. Cesium-03:洪水淹没

    Cesium-01:Vue 中基础使用 Cesium-02:飞机模型简单点对点飞行 Cesium-03:洪水淹没 前言 最开始想做洪水淹没的话,查了一些资料.又基于不同的实现的,如 ArcScene ...

  10. c++ trivial, standard layout和POD类型解析

    目录 1. trivial类型 2. standard layout类型 3. 集大成者,POD(Plain Old Data)类型 4. 测试代码 1. trivial类型 占用一片连续的内存,编译 ...