背景介绍:
上述题目就是经典的KMP算法的模版题
首先我们介绍传统的暴力做法,不过显示是ac不了这道题的
暴力做法: 两个串依次比较
该算法的时间复杂度对于本题而言 就是O(mn)
如何优化呢?
对于下面这种情况而言:
当浅蓝色框内的部分匹配,A[5]!=B[4]时:
暴力做法 就是 A串 i=2 这个元素 与 B串首字母 重新进行匹配
显然 这个过程每次遇到不匹配的 都是A串向前一位 B串从头开始比较 非常繁琐
故这里 就有大佬创造了 KMP算法!!!!!!!!!!
其核心就是优化上述 过程
先简单解释一下 KMP算法 主要做了什么工作哈
a b串 黑色线之间部分已经匹配,后面一个元素(绿色圈里面的)不匹配
此时暴力做法就是:
a串向后移动一个位置,与b串从头开始比较,及也就是从紫线部分从头开始比较
KMP的思想就是,a串向后最多能移动几个位置,而保证不出错。
这里引入 最长 前缀 后缀 相等 的概念 (这里就不详细介绍了 帖子很多 也很简单 以下用数组ne[]表示)
首先证明 x的 存在且必要:
(这部分绿线长度为非负 >=0)即就是 不存在匹配的 前后缀的串
又因为黑色线之间的部分 a b串本身就是匹配的 故可以得到:
ZW这两部分 是匹配的
因为x是非负的 故一定存在
此时直接比较红色框内的这部分即可 因为前面已经是配的了
一次KMP在字符串匹配上面的效率很快 时间复杂度 O(n)!!!!!
以下这部分 重中之重!!!!!!!-
理解了 KMP算法的思想 下面主要就是求 ne数组 存储前后串匹配位置的数组即可
用到的方法就是 递推的方法
首先解释一下 ne数组 里面的数字表示啥: 假设ne[ j ]=k;
意思是: 数组下标 从 0 1 2 一直到 j的这部分 其 前缀 后缀 的长度为 k+1
有了这个概念 我们定义不匹配为 -1 表达式ne【j】=k; 且前缀后缀长度均小于其自身 即 k<j
所以已知 ne【0】= -1;
假设j=n时已知,ne【n】=x
红的括号内的两部分 匹配 即为前缀 后缀 字符串 (且为最长哈 定义保证了)
注释:其中 n x 为数组下标 且数组下标 从0开始
+
来计算j=n+1时 有点数学归纳法的意思了哈 这也是 为啥代码体现为 递推
这里就需要分情况讨论了:
第一种情况:arr【n+1】==arr【x+1】
此时ne【n+1】=x+1 结束
第二种情况:arr【n+1】!=arr【x+1】
此时就要自己去寻找 0~ n+1 下标是的 最长相等前后缀的位置了
暴力做法(为什么每次都要提到暴力做法,因为好的做法 都是从暴力做法去优化出来的!!!!)
首先看 挪一步 是否匹配:
挪动两步:
………………
依次向下总到找到 ne【n+1】=y; y=-1,0,1,2……x 最大取到x嘛 这里范围也不重要 反正能找到就行了。
观察如下关系:
A点坐标n c点坐标为x B点坐标为y-1
绿线与arr的三个焦点分别为1 2 3 注意 不是坐标哈
已知的ne【n】=x (我们的假设 别忘了)
所以:1A段与2C段是匹配的
又因为 ne【n+1】=y
所以:1A段与3B段是匹配的
所以2C段与3B段也是匹配的 所以这个东西出来 你们有什么感悟嘛 !!
是不是关系就得出来了:
即 B点是A点 不断ne【】 取出来的结果 结束条件就是arr【n+1】==arr【y】 当然了 y=-1的话 就是不存在这个点 (整个移到头就行)
当然了 也有找不到的情况:
就是到这 应该理解ne数组怎么求了
核心思想就是 递推 直到ne【0】 ne【1】 可以推出ne【2】……一直递推
//求Next数组 表示最长 相等前缀后缀字符串
for(int i=1,j=0;i<=n-1;++i)
{
while(j>0 && arr[i]!=arr[j] )
j=Next[j-1]+1;
if( arr[i]==arr[j])
j++;
Next[i]=j-1;
}
解释一下: 因为ne【0】=-1 这是已知的 所以 i就从1 开始 也可以理解为上下要错开一个
代码不要从头开始 分析 只要记得 我们求 i这个位置时(求ne【i】) 前面 0~i-1 是已知的就行
这样方便初学者理解
所以这一步 就是我们上面提到的
结束条件有两个 找到了这两个点相等 或者就是没找到 因为没找到 ne【】=-1 所以j=0;
这个意思就是: 经过上面我找到了 他两匹配 是不是就要看下一位是不是匹配 所以j++了 因为i++ 会在循环条件里面判断 所以这里就不用多判断了
这里最后一步就是 因为上面我们找到了 ne【i】的值 所以来给他赋值
为什么减一呢 因为上面我们有个if 使得j++了
如果j=0 就是没找到 匹配的 所以赋值为-1;
看到这里ne数组就取出来了
最后就是 字符串的匹配过程:
#include<iostream>
using namespace std;
const int M=1000010;
const int N=100010;
char arr[N],brr[M];
int Next[N];
int main()
{
Next[0]=-1;
int n,m;
cin>>n;
scanf("%s",arr);
cin>>m;
scanf("%s",brr);
//求Next数组 表示最长 相等前缀后缀字符串
for(int i=1,j=0;i<=n-1;++i)
{
while(j>0 && arr[i]!=arr[j] )
j=Next[j-1]+1;
if( arr[i]==arr[j])
j++;
Next[i]=j-1;
}
//匹配过程
for(int i=0,j=0;i<=m-1;++i)
{
while(j>0 && brr[i]!=arr[j])
j=Next[j-1]+1;
if( brr[i]==arr[j])
j++;
if(j==n)
{
printf("%d ",i-n+1);
j=Next[j-1]+1;
}
}
return 0;
}
brr为 我们被搜索的 数组 arr为我们搜索的数组
匹配过程和 求ne完全类似 因为我们求ne其实就是 两个数组来做的 这里只不过就是两个数组不一样了
这里还是别从头分析 只要写出递推关系就行了
假设i-1 j-1 前面部分匹配 来看i j 两个位置 如果不相等 就一直取arr的 ne数组就行 直到这两个位置匹配 或者 没找到匹配的
如果匹配了 就直接就看下一个位置就行
这里的判断条件 就与上面求ne数组的地方不太一样
主要就是看我们arr的数组是不是匹配完了
下标都是从零开始 到n-1 这里为什么判断n呢
因为 上面的if条件 如果arr【n-1】匹配了 那就会使用j++ 所以会多一个
然后就是打印 brr匹配的起始下标
这里的下一步就是 接着搜索
首先来看一下暴力的话 这一步怎么写
for(int i=0,j=0;i<=m-1;++i)
{
while(j>0 && brr[i]!=arr[j])
j=Next[j-1]+1;
if( brr[i]==arr[j])
j++;
if(j==n)
{
printf("%d ",i-n+1);
i=i-n+1;
j=0;
}
}
就是 brr数组 从匹配位置的下一个点 和 arr从头开始 我们最开始的那个过程
怎么优化呢 画个图就明白了
这两部分已经匹配完成
下一部分匹配 无非就是这两种情况:
观察左边那一个 不就是 前后缀匹配 ne嘛
右边那一个不就是 ne到-1了么
所以这里就直接优化
就行
最后放一下 题目ac的截图吧
KMP算法 就我自己第一遍学习 编写了此博客 欢迎各位大佬批评指正
我是这么理解 KMP算法的 以及编写代码