背景引入
在某些时候,我们常常会出现字符串匹配的问题。
例如:对于字符串s1,s2,判断s2在s1中出现的次数,正常来说,如果暴力枚举的话,我们需要使用O(n*m)的时间复杂度。
有三位大佬就想出了KMP算法,保证了O(n+m)线性复杂度。
算法讲解
基本思想
KMP算法的基本思想就是,当我们在字符串匹配的时候,如果出现了不匹配的情况,我们并不需要重新枚举,而是通过next数组(之后再说如何生成的),从某个特定的位置开始匹配。而如何知道这个特定的位置,便是next数组。
我们先看一组样例:
主串: ABABABCAA
模拟串:ABABC
当我们正常匹配时,会出现匹配到C时出现问题,那么正常来说,我们需要回退主串到B然后继续匹配:
回退 然后重新匹配
主串: ABABABCAA
模拟串: ABABC
但这样是很麻烦的,通过观察,我们可以这样移动模拟串:
通过观察的移动,且前面两个AB并不需要匹配
只需要匹配后面的三个数即可
主串: ABABABCAA
模拟串: ABABA
我们发现,如果回退,然后重新匹配会浪费很多时间,但我们可以通过next数组来达到最优的移动,且重复的匹配并不需要。
这就是next数组,next数组存储的当出现字符串不匹配的时候,我们该如何移动。
这个便是已经知道了next数组以后,程序的实现:
//i代表s1的起点 j代表s2的第几个位置
for(int i=1,j=0; i<=n; i++) {
//如果匹配失败 通过next数组j不断往回退 直到可以继续匹配
while(j && s1[i]!=s2[j+1]) j = next[j];
//匹配成功 j++
if(s1[i] == s2[j+1]) j++;
//当j到达字串的末尾,匹配成功
if(j == m) {
//通过next数组移动
j=next[j];
cout<<i-m+1<<'\n';
}
}
如何生成next数组
那么我们既然已经知道了next数组的作用了,那么该如何生成next数组呢?
继续看例子:
主串: ABABABCAA
模拟串:ABABC
我们会发现,我们之所以会移动两个字符,是因为移动的AB和ABABC中加粗的字符是一样的,对于字串的前四个字符,它们拥有一个相同的前缀和后缀AB,所以我们才能移动两个字符。
next数组的本质就是相同前后缀的最长长度
继续看例子:
模拟串: ABABC
next: 00120
如何通过代码实现next数组,我们可以使用暴力枚举的方式,但这样时间复杂度很高,我们可以通过递推的方式来实现:
//求next数组
// 从2开始 因为ne[0]=0,ne[1]=0;
for(int i=2,j=0; i<=m; i++) {
//如果出现不匹配的情况或者j为0就是出现意外了
while(j && s2[i]!=s2[j+1])
j = ne[j];
if(s2[i] == s2[j+1])
j++;
ne[i] = j;
}
递归的本质就是通过已知的数据来推出后面的数据。
例子
【模板】KMP
题目描述
给出两个字符串
s
1
s_1
s1 和
s
2
s_2
s2,若
s
1
s_1
s1 的区间
[
l
,
r
]
[l, r]
[l,r] 子串与
s
2
s_2
s2 完全相同,则称
s
2
s_2
s2 在
s
1
s_1
s1 中出现了,其出现位置为
l
l
l。
现在请你求出
s
2
s_2
s2 在
s
1
s_1
s1 中所有出现的位置。
定义一个字符串
s
s
s 的 border 为
s
s
s 的一个非
s
s
s 本身的子串
t
t
t,满足
t
t
t 既是
s
s
s 的前缀,又是
s
s
s 的后缀。
对于
s
2
s_2
s2,你还需要求出对于其每个前缀
s
′
s'
s′ 的最长 border
t
′
t'
t′ 的长度。
输入格式
第一行为一个字符串,即为
s
1
s_1
s1。
第二行为一个字符串,即为
s
2
s_2
s2。
输出格式
首先输出若干行,每行一个整数,按从小到大的顺序输出
s
2
s_2
s2 在
s
1
s_1
s1 中出现的位置。
最后一行输出
∣
s
2
∣
|s_2|
∣s2∣ 个整数,第
i
i
i 个整数表示
s
2
s_2
s2 的长度为
i
i
i 的前缀的最长 border 长度。
样例 #1
样例输入 #1
ABABABC
ABA
样例输出 #1
1
3
0 0 1
提示
样例 1 解释
。
对于
s
2
s_2
s2 长度为
3
3
3 的前缀 ABA
,字符串 A
既是其后缀也是其前缀,且是最长的,因此最长 border 长度为
1
1
1。
数据规模与约定
本题采用多测试点捆绑测试,共有 3 个子任务。
- Subtask 1(30 points): ∣ s 1 ∣ ≤ 15 |s_1| \leq 15 ∣s1∣≤15, ∣ s 2 ∣ ≤ 5 |s_2| \leq 5 ∣s2∣≤5。
- Subtask 2(40 points): ∣ s 1 ∣ ≤ 1 0 4 |s_1| \leq 10^4 ∣s1∣≤104, ∣ s 2 ∣ ≤ 1 0 2 |s_2| \leq 10^2 ∣s2∣≤102。
- Subtask 3(30 points):无特殊约定。
对于全部的测试点,保证 1 ≤ ∣ s 1 ∣ , ∣ s 2 ∣ ≤ 1 0 6 1 \leq |s_1|,|s_2| \leq 10^6 1≤∣s1∣,∣s2∣≤106, s 1 , s 2 s_1, s_2 s1,s2 中均只含大写英文字母。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char s1[N],s2[N];
int n,m;
int ne[N];
int main() {
//从下标为1开始
cin>>s1+1;
cin>>s2+1;
n=strlen(s1+1),m=strlen(s2+1);
//求ne数组
// 从2开始 因为ne[0]=0,ne[1]=0;
for(int i=2,j=0; i<=m; i++) {
//如果出现不匹配的情况或者j为0就是出现意外了
while(j && s2[i]!=s2[j+1])
j = ne[j];
if(s2[i] == s2[j+1])
j++;
ne[i] = j;
}
//i代表s1的起点 j代表s2的第几个位置
for(int i=1,j=0; i<=n; i++) {
while(j && s1[i]!=s2[j+1]) j = ne[j];
if(s1[i] == s2[j+1]) j++;
//匹配成功
if(j == m) {
j=ne[j];
cout<<i-m+1<<'\n';
}
}
//最长border其实就是ne记录的数据
for(int i=1; i<=m; i++) cout<<ne[i]<<" ";
return 0;
}
推荐视频
欢迎大佬指出错误。