模板题:洛谷P3375
暴力的复杂度O(mn)
KMP复杂度O(n)
Part 1: 匹配的思路优化:
txt表示长串,pat表示短串,目标是在长串中找出短串的所在位置
记: i 为txt下标, j 为pat下标, 均表示当前待判断的位置, 字符串下标从0开始。
If ( txt[i]==pat[j] ){//如果匹配
i++,j++;//就下一位置
}
else{//如果不匹配
j=k;//令j=k
//其中, k符合:pat[0,k-1]==txt[i-k,i-1]
//即pat的长度为k-1的前缀 与 txt从i-1位置再往前推k-1个前缀 一致
//注:如果没有相等前缀,k=-1;
i++;
}
#为什么这样的思路匹配,最“聪明”,具体解释和动态效果图点击这里 [这篇知乎解释得很好]
那么,这个方法巧妙处在于令j=k,难点也在于如何快速找到这样的k?
“nex数组” 帮助我们进行这一步的操作👇
Part 2:nex数组 含义与构造
#nex数组处理部分推荐食用这里 [图文结合很不错]
**目标:**令nex[j]表示 j点处失配时应该跳到的k值
根据nex数组需要达成的作用 先思考的两件事:
- 如果要符合pat[0,k-1]==txt[i-k,i-1],一定符合pat[k-1]==txt[i-1],
也就是说,如果适配位置的前一个字母为“A”,那一定至少要跳到pat串中上一个为"A"的位置;如此一直向前跳,直到符合条件。 - 由此推出,符合条件的nex[j]==k一定符合:pat[0,k-1]==pat[j-k,j-1]
根据这样的理解
=>nex数组的初始化处理一定有一个状态转移:
if(pat[k]==pat[j]) nex[j+1]=nex[j]+1;
这样转移的原因就是,这样可以保证符合上面推出的第二条
证明:
∵k=nex[j] 即 pat[0,k-1]==pat[j-k,j-1]
∴ if(pat[k]==pat[j]) 则pat[0,k]==pat[j-k,j]
∴nex[j+1]=k+1=nex[j]+1;
⇒ nex数组的完整状态转移方程:
int j=0,nex[0]=k=-1;//j表示从pat的0点开始判断
//根据Part1讲述,如果没有符合条件的k,则令j=-1,即k的初始值是-1
while(j<pat.length()-1){
if(k==-1||p[j]==p[k]) nex[++j]=++k;//匹配,下一位
else k=nex[k];//如果不等,k往前跳(符合上面推出的第一条)
}
注:写成nex[++j]=++k 而不能写nex[j+1]=nex[j]+1的原因是,如果k是-1进入这句,那就要从0开始。
Part 3:KMP实现
根据上述思路,KMP分为两个核心部分
- nex数组的构造
- 与主串的匹配
PS1. 其实完整地来看,很像pat自己对着自己做了一边KMP,然后再对txt做一遍KMP ⇒ 也许这样想可以方便记忆233
PS2. 根据前文对于nex性质的推演,归纳一下nex数组的本质: nex[i]表示pat串[0,i-1]的部分,前缀==后缀的最长长度
KMP模板代码:
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+5;
char txt[N],pat[N];
int nex[N];
int main(){
cin>>txt>>pat;
int len1=strlen(txt),len2=strlen(pat);
//nex数组
int j=0,k;nex[0]=k=-1;
while(j<len2){
if(k==-1||pat[j]==pat[k]) nex[++j]=++k;
else k=nex[k];
}
//kmp
int i=0;j=0;
while(i<len1){
if(j==-1||txt[i]==pat[j]) i++,j++;
else j=nex[j];
if(j>=len2){cout<<i-len2+1<<endl;j=nex[j];}
}
for(int i=1;i<=len2;i++)
cout<<nex[i]<<" ";
return 0;
}
//ppss: 考研专业课大纲里竟然看到有KMP,震惊