震惊,KMP加上扩展KMP的学习笔记字数竟然破万了(令人窒息)
KMP部分
例子:一个文本串A,一个模式串B,A的长度为n,B的长度为m,求B在A中出现的位置。()
题目链接:洛咕3375 【模板】kmp字符串匹配
暴力:枚举文本串中的位置,暴力比较A的这个区间是否与B相同。时间复杂度最坏情况是A,B都只有一种字符(比如A是aaaaa,B是aaa),此时时间复杂度为。
燃鹅,所以要优化到线性。
发现暴力比较的过程中有很多冗余的操作,所以考虑优化这个过程。
一、next数组
令表示模式串B中,假设前缀构成的字符串为,使的前缀与后缀相同的最大长度,不算前、后缀为本身的情况(也就是规定)。
其中字符串的前缀表示,后缀表示
例子:字符串为ABABA。
珂以得出,。
next[1]:考虑的是。因为前、后缀不能取整个字符串,所以next[1]=0
next[2]:考虑的是。因为不等于,所以next[2]=0
next[3]:考虑的是。最长前、后缀相等的长度为1,此时前缀为,后缀为。 前、后缀相等的长度不能为2,因为长度为2的前缀为,后缀为"BA"。
next[4]:考虑的是前、后缀长度均为2时,前、后缀均为。前、后缀长度为3时,前缀为,后缀为。
next[5]:考虑的是。同理珂得,使前、后缀相同的最大长度为3,即前后缀均为。
求出next数组的方式:
让B自己与自己比较:
比如,现在要求出它的next[5]。
由于不能前、后缀为整个字符串,所以先把第二个B串往右移一格:
ABABA
ABABA
忽略空出的部分,那么珂以发现,比较的是第一个B串的后缀,和第二个B串的。
发现并不相同,所以再把第二个B串往右移一格:
ABABA
ABABA
同样地比较两串都非空的部分,即比较A串的后缀"ABA"和B串的前缀"ABA"。
因为后缀和前缀相同,所以next[5]=3。
正确性证明:
让B串和自己比较,把第二个B串往右移动一格,那么非空部分就分别表示第一个B串的后缀和第二个B串的前缀,然后让第一个B串的后缀和第二个B串的前缀比较。
如果第一个B串的后缀和第二个B串的前缀相同,那么表示这个长度是最大的能让B串的前后缀相同的长度。
然鹅这样仍然不是线性,所以还要优化:
假设对于一个字符串,需要求出的值。
考虑一个一个把字符加进去,那么现在已经加入了前个字符(如图所示)。
由数组的定义,这个字符串的前个字符和后个字符相同(如图所示)。
然后加入第个字符,如图,蓝色方框表示第个字符。令。
情况1:第个字符与第个字符相同
因为已经是让前个字符的前、后缀相同的最大长度,
因为,所以若第个字符与第个字符相同,则。
证明:若存在比更长的长度,使前个字符前、后缀相同,那么不是最大长度,所以矛盾。
因此这样有正确性qwq。
情况2:第个字符与第个字符不相同
然后考虑一个孙臭的情况:第个字符和第个字符不同。
这种情况不符合(因为前后缀不一样)。
珂以证明,最长的长度一定不超过:
若有比更长的长度使前个字符的前后缀相同,则假设长度为。
根据数组的定义,前个字符与后个字符相同,那么珂以推出前个字符与倒数第个字符至第个字符相同,所以(因为),与矛盾。
因此。
所以我们需要从到中找到一个长度,使得个字符中,前个字符和后个字符相同。
不妨去掉“前个字符”与“后个字符”两者的最后一个字符,即前个字符与倒数第个字符至第个字符分别相等(如图所示)。
所以,前个字符中,长度为的前缀、后缀相等。
next数组的定义:表示使前个字符前缀、后缀相等的最大长度。
所以此时我们让,然后检验第个字符是否与相等。
如果相等那么回到情况1,否则回到情况2。
代码实现:
int j=0;
for(int i=2; i<=n; i++) { //next[1]=0
//此时j存储的使next[i-1]的值
while(j>0 && str[j+1]!=str[i]) { //注意判断j>0
//若第j+1个字符不等于第i个字符
//那么j+1不能使前i个字符前后缀相同,应继续循环(重复情况2)
j=nxt[j];
}
//判断,如果第j+1个字符与第i个字符相同,那么next[i]=j+1
if(str[j+1]==str[i]) j++;
nxt[i]=j;
}
二、求出B在A中的位置
燃鹅仅知道一个字符串的数组是布星的,再回顾问题:
一个文本串A,一个模式串B,A的长度为n,B的长度为m,求B在A中出现的位置。()
同样遍历A,假设当前遍历到字符串A的第个字符,且A的前个字符的后个字符与B的前个字符相同。
假设不存在更大的,使得A的后位与B的前位相同。
如图,若B串的第个字符与A串的第个字符相同,那么现在B就匹配到了第个字符。(不珂能匹配到更多字符,证明过程类似求数组中的情况1,这里不写了)
若第个字符与第个字符不相同,B串就不能匹配位。因此现在需要求出在A串加入第个字符后,B串与A串的后缀最多匹配几位。
首先可以知道能匹配的字符数不会超过(证明过程类似求的情况1)。
所以应该把B数组往右移。
假设移到如图所示的位置时,A串的后位与B串的前位相同。
那么我们珂以发现,新的B串(图中的new B)的前个字符,和原B串的前个字符的后个字符重合了。
而因为新的B串和原B串相同,所以B串的前个字符的前个和后个字符相同。
考虑的定义:表示前个字符的前后缀相同的最大长度。
因此的最大值为!
但是还需要保证B串的第个字符与A串的第个字符相同。
所以类似求数组的过程,每次让跳到的位置,然后判B串第个字符是否与A串第个字符相等即珂。
代码实现:
for(int i=1; i<=n; i++) {
//跳到第一个B串的第j+1个字符与A[i]相等的位置(或跳到0,此时表示最大的匹配长度为0)
while(j>0 && B[j+1]!=A[i]) j=nxt[j];
if(B[j+1]==A[i]) j++;
if(j==m) {
printf("%d\n",i-m+1); //输出B在A串中的起始位置
j=nxt[j]; //j已经匹配到最后一位,所以重新开始匹配
}
}
(可能出现的)疑问
Q:以第二部分求B在A中的位置为例,每次都是把跳到的位置,也就是说,会先后变为 那么会不会错过一些本来能使B的前个字符与A的后个字符成立的?
A:不会。错过本来能使B的前个字符与A的后个字符成立的,意味着错过使B前后缀相等的长度。
可以证明,是最大的使前个字符前后缀相等的长度,是第二大的使前个字符前后缀相等的长度,是……
过程如下:
由定义,是最大的使前个字符前后缀相等的长度,没毛病qwq。
假设第二长的使前个字符前后缀相同的长度不是,而是比长的长度(如图中红线所示)。
假设红线长度为。因为红线与前个字符的前个、后个字符均相等,所以应为,矛盾。
所以比大的是不存在的qwq
因此是第二大的使……(不想打了)的长度。
同理是……(懒qwq)
这说明从开始不断取相当于从大到小遍历让前后缀相同的长度,故一定能取到这些长度中最大的一个qwq。
毒瘤代码
//Luogu P3375
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
const int Size=1000005;
int n,m,nxt[Size];
char A[Size],B[Size];
int main() {
scanf("%s",A+1);
scanf("%s",B+1);
n=strlen(A+1);
m=strlen(B+1);
int j=0;
for(re i=2; i<=n; i++) {
//此时j存储的使next[i-1]的值
while(j>0 && B[j+1]!=B[i]) { //注意判断j>0
//若第j+1个字符不等于第i个字符
//那么j+1不能使前i个字符前后缀相同,应继续循环(重复情况2)
j=nxt[j];
}
//判断,如果第j+1个字符与第i个字符相同,那么next[i]=j+1
if(B[j+1]==B[i]) j++;
nxt[i]=j;
}
for(re i=1; i<=n; i++) {
//跳到第一个B串的第j+1个字符与A[i]相等的位置(或跳到0,此时表示最大的匹配长度为0)
while(j>0 && B[j+1]!=A[i]) j=nxt[j];
if(B[j+1]==A[i]) j++;
if(j==m) {
printf("%d\n",i-m+1); //输出B在A串中的起始位置
j=nxt[j]; //j已经匹配到最后一位,所以重新开始匹配
}
}
for(re i=1; i<=m; i++) printf("%d ",nxt[i]);
return 0;
}
常用推论
推论1.表示第二大的前个字符的前后缀相同的长度,表示第三大的使……的长度。
证明:上面已证。
推论2.用若干个串拼在一起(不重叠)把整个字符串覆盖,这样的串的长度最小为。
证明:若可以找到更小的长度覆盖整个字符串,则可以证明应该更大。图略。
扩展KMP部分
网上的题解大多是下标从0开始,看着不刁惯并且难受……
推荐一个讲得很好的博客(不过下表是从0开始的):传送门
给定长度为的串,长度为的串,令表示,同理。
令表示与的最长公共前缀,表示与的最长公共长度。
(即是串与的第位之后的串的最长公共前缀,为串与的第位之后的串的最长公共前缀,注意的定义发生了改变)
题外话:为什么这个看起来奇怪的东西叫扩展kmp呢?因为当时,就相当于在中出现了……
规定:
为了方便(和写代码的时候不发生奇怪的变量重名),写作,写作。
表示的第位,同理。
暴莉求显然是的(同的暴莉),考虑优化。
假设现在已经求出了到,现在要求(先不管数组怎么求)。
假设之前让与所有的匹配时,串匹配到的最远位置为,且最远位置是从匹配到的。
也就是说,,且。
观察此图,发现。
而现在我们需要求出与的最长公共前缀。
根据数组的定义,表示与的最长公共前缀。
设,然后分类讨论:
一、
表示,因为在左边。
那么表示串中(如图)。
此时,因为
(1)
(2)若,则说明,则,与的定义不符。
因此。
二、
如图,是一段没有比较过的位置,无法确定与是否相等。
而,所以从的第位,的第位开始暴力匹配,失配时表示这个长度是的值。
讲到这里,读者应该能写出求解数组的方法。
我的代码写得比较毒瘤,仅供参考qwq
void GetExtend() {
int j=1;
while(j<=n && j<=m && s[j]==t[j]) j++;
ext[1]=j-1;
int pos=1;
for(re i=2; i<=n; i++) {
//这个地方比较玄学,不能写i+nxt[i-pos+1]-1<=pos+ext[pos]-1
if(i+nxt[i-pos+1]<=pos+ext[pos]-1) {
ext[i]=nxt[i-pos+1];
} else {
j=max(pos+ext[pos],i);
while(j<=n && j-i+1<=m && s[j]==t[j-i+1]) {
j++;
}
ext[i]=j-i;
pos=i;
}
}
}
求解数组的方法比较类似。因为表示的是与的最长公共前缀,而表示的是与的最长公共前缀,所以求时就让和本身执行求的过程即珂。
由于求过程中,需要用到的的下标都比小,所以不会出现调用没有被求出的值。
void GetNext() {
nxt[1]=m;
int j=1;
while(j<m && t[j]==t[j+1]) j++;
nxt[2]=j-1;
int pos=2;
for(re i=3; i<=m; i++) {
if(i+nxt[i-pos+1]<=pos+nxt[pos]-1) {
nxt[i]=nxt[i-pos+1];
} else {
j=max(pos+nxt[pos],i);
while(j<=m && t[j]==t[j-i+1]) {
j++;
}
nxt[i]=j-i;
pos=i;
}
}
}
毒瘤代码
输出数组:
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
const int Size=100005;
int n,m,nxt[Size],ext[Size];
char s[Size],t[Size];
void GetNext() {
nxt[1]=m;
int j=1;
while(j<m && t[j]==t[j+1]) j++;
nxt[2]=j-1;
int pos=2;
for(re i=3; i<=m; i++) {
if(i+nxt[i-pos+1]<=pos+nxt[pos]-1) {
nxt[i]=nxt[i-pos+1];
} else {
j=max(pos+nxt[pos],i);
while(j<=m && t[j]==t[j-i+1]) {
j++;
}
nxt[i]=j-i;
pos=i;
}
}
}
void GetExtend() {
int j=1;
while(j<=n && j<=m && s[j]==t[j]) j++;
ext[1]=j-1;
int pos=1;
for(re i=2; i<=n; i++) {
if(i+nxt[i-pos+1]<=pos+ext[pos]-1) {
ext[i]=nxt[i-pos+1];
} else {
j=max(pos+ext[pos],i);
while(j<=n && j-i+1<=m && s[j]==t[j-i+1]) {
j++;
}
ext[i]=j-i;
pos=i;
}
}
}
int main() {
// freopen("data.txt","r",stdin);
// freopen("WA.txt","w",stdout);
scanf("%s",s+1);
scanf("%s",t+1);
n=strlen(s+1);
m=strlen(t+1);
GetNext();
GetExtend();
for(re i=1; i<=n; i++) {
printf("%d ",ext[i]);
}
return 0;
}
/*
dadab
dad
*/
例题
还没写题,待续qwq