kmp问题
·>kmp问题是指在一个长字符串中寻找能和已知字符串匹配位数的问题。
- kmp算法的原理
- next数组\kmp数组
- kmp算法的实现
- next数组\kmp数组的实现
- 失配传送的实现
- 实例
【kmp算法的原理】
**引例:**给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。
输入样例#1 | 输出样例#1 |
---|---|
ABABABC | 1 |
ABA | 3 |
0 0 1 |
*首先从朴素算法开始。我们会先将s1的第一位和s2的第一位进行比较,发现A与A匹配,然后比较s1 & s2第二位……直到发现s1 & s2的第三位A与A相等,我们输出第一次的位置1,然后返回到s2的第一位,和s1的第二位进行比较。到现在我们可以发现了,朴素算法的缺点就在这里:每一次失配后或者完成匹配后都需要返回到最初的位置的下一个位置。而kmp问题就是优化了这个步骤。
*kmp算法通过使用next[]
数组(或者是kmp[]
数组)来记录在s2的某一个位置失配以后可以跳回到前面的哪一位,然后在失配后调用数组跳回达到速度优化。
[next数组\kmp数组]
这里引用一个视频 (阿三讲的真实好)
简单来说,kmp数组就是将待匹配字符串s2中进行前缀和后缀的匹配。由于在s1失配位的字符之前的一段字符都是和相对应的s2中的一部分字符串匹配的,所以只要寻找s1中在这一位之前的字符串中是否存在一段相等的字符,例如ABCABTTTTT中,如果失配位置是第一个T,那么在T之前的字符串中可以发现B与B匹配,AB与AB匹配,所以在执行跳回的过程中有这样两个选择,从s2的第一B开始重新匹配和从s2的第一个AB开始匹配。但是我们可以发现从AB开始匹配显然是最优解,所以我们这里定义的next[]
数组记录的就是失配以后可以跳回的最末的前缀位置的下一位。
|0|1|2|3|4|5|
|A|B|C|A|B|T|
【kmp算法的实现】
[next数组\kmp数组的实现]
for(int i=1;i<len2;i++){//i从1向末移动
while(s2[i]!=s2[j]&&j) j=next[j];
//s2[i]!=s2[j]表示如果现在已知的最长前缀末位置上的字符与索引字符i不匹配,
//那么就不断将可以使用的最长前缀末位置传送到从s2[1]~s2[j-1]的字符串中可
//以匹配的一对最大前后缀中的最大前缀的末位置,或者找不到可以匹配的前后缀
//则直到j变为0,遍历完全。
if(s2[j]==s2[i]){
//判断当前索引字符与经过上面一步处理后的结果到底是前一种得到匹配的可能还
//是后一种没有得到匹配的可能
next[i+1]=++j;
//这种就是前一种得到匹配的可能,这时候最大匹配前缀末位后一位已经变成
//最大匹配前缀末尾,将j+1以后移动到新的最大匹配前缀末尾,并且将他附
//加给next数组
}else{
next[i+1]=0;
//这种就是后一种没有得到匹配的可能,这时候由于失配只能回到0
}
//上面一步是对第一个while的结果判断和补充,如果执行else则k已经是0了
}
//创建next[]结束
[失配传送的实现]
for(int i=0;i<len1;i++){
//cout<<"i="<<i<<"j="<<j<<endl;
while(s2[j]!=s1[i]&&j) j=next[j];
//s2[j]!=s1[i]表示如果现在待匹配字符和予匹配字符不符合的话就传送到前一
//个可以匹配的最大前缀处,要么找到后达到可以匹配的最大前缀后一位,要么
//传送到匹配源串的头
if(s1[i]==s2[j]){
j++;//如果成功匹配这一位的话就将源串指针向后移,用来适应for循环中i
//指针的移动
}
if(j==len2){
cout<<i-len2+2<<endl;
//如果检测到源串指针达到源串最后一位,则得到一个满足的位置。
}
}
[程序]
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<vector>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define rep(i,a,b) for(register int i = (a);i <= (b);++i)
#define per(i,a,b) for(register int i = (a);i >= (b);--i)
typedef long long ll;
typedef unsigned long long ull;
using std::string;using std::cin;using std::cout;
char str[1000010],m[1000010];
int len_str,len_m;
int kmp[1000010];
int main(){
std::ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
//freopen("in.in", "r", stdin);
cin >> m+1 >> str+1;
len_m = strlen(m+1) , len_str = strlen(str+1);
int now = 0;
rep(i,2,len_str){
while(now && i <= len_str && str[now+1] != str[i]) now = kmp[now];
if(str[now+1] == str[i]) kmp[i] = ++now;
}
now = 0;
rep(i,1,len_m){
while(now && i <= len_m && str[now+1] != m[i]) now = kmp[now];
if(str[now+1] == m[i]) ++now;
if(now == len_str){
cout << i - now + 1 << '\n';
}
}
rep(i,1,len_str) cout << kmp[i] << " ";
return 0;
}```
### 【实例】
#### P4391 [BOI2009]Radio Transmission 无线传输
```cpp
#include<iostream>
#include<cstring>
using namespace std;
char s2[2000001];
int next[2000001];
//就是kmp数组,用来记录到此为止匹配出错时可以与下标之前的后缀匹配的最长前缀末位
//置的后一个,下一次直接从赋值下一个位置开始匹配
int main(){
int i,j,n;//i用来在进行匹配任务的字符串中进行索引,从1向末移动
j=0;
cin>>n;
cin>>s2;//s1的长度
for(int i=1;i<n;i++){//i从1向末移动
while(s2[i]!=s2[j]&&j) j=next[j];
//s2[i]!=s2[j]表示如果现在已知的最长前缀末位置上的字符与索引字符i不匹配,
//那么就不断将可以使用的最长前缀末位置传送到从s2[1]~s2[j-1]的字符串中可
//以匹配的一对最大前后缀中的最大前缀的末位置,或者找不到可以匹配的前后缀
//则直到j变为0,遍历完全。
if(s2[j]==s2[i]){
//判断当前索引字符与经过上面一步处理后的结果到底是前一种得到匹配的可能还
//是后一种没有得到匹配的可能
next[i+1]=++j;
//这种就是前一种得到匹配的可能,这时候最大匹配前缀末位后一位已经变成
//最大匹配前缀末尾,将j+1以后移动到新的最大匹配前缀末尾,并且将他附
//加给next数组
}else{
next[i+1]=0;
//这种就是后一种没有得到匹配的可能,这时候由于失配只能回到0
}
//上面一步是对第一个while的结果判断和补充,如果执行else则k已经是0了
}
cout<<n-next[n];
}
过于慵懒甚至直接复制
P4173 残缺的字符串(这个只有80分)
本来觉得这道题真jier简单,居然是省选,结果越想越难受一做分能骗但a不了(哭了,看了题解发现尽是FFT??!!那好吧,我就把能骗到分的程序来了。
# include<iostream>
# include<cstring>
# include<cstdio>
using namespace std;
int ans[3000001];
string a,b;
int main(){
int l1,l2;
cin>>l1>>l2>>a>>b;
for(int i=0;i<=l2-l1;++i)
{
bool fl=1;
int j=i,l=0;
while(l<l1)
if(a[l]==b[j]||a[l]=='*'||b[j]=='*') j++,l++;
else
{
fl=0;
break;
}
if(fl) ans[++ans[0]]=i+1;
}
cout<<ans[0]<<endl;
for(int i=1;i<=ans[0];++i)
cout<<ans[i]<<" ";
return 0;
}