kmp算法是一种非常经典的处理字符串问题的算法,它解决的是最经典的一类字符串问题,即对于给定的母串s1和子串s2,s2是否在s1中出现过,如果有请返回s1中s2开始的位置,首先对于解决这个问题本身并不难,哪怕是初学者也很容易想到可以一个位置一个位置去匹配,一旦出现不符合的情况,就把此时的开头向后移动一个位置然后继续依次比较,直到完全匹配成功就返回开头,这是一个最简单且暴力的o(n^2)的算法,而kmp在时间上进行了大大的优化。
考虑到如果有一个位置不符合就从头再来,难免出现浪费,会不会可能出现无需重复比较的情况,那么如果目标子串匹配失败位置的前缀字符串的前缀和后缀有一个最长的匹配长度,首先既然可以来到这个位置,说明前面的字符串肯定完全匹配,那么此时把子串的前缀和母串的后缀对齐再继续比较下一个位置就可以省去了大量比较的时间,其实这就是kmp的本质,但是出现了几个问题,第一,为什么这样跳过一些位置就一定没有错过答案呢,第二,我怎么统计相同的前缀后缀呢,这个过程也并不简单呀,更何况我要统计每个位置的前缀字符串的最长前缀后缀匹配长度。
对于第一个问题,因为我们将子串的前缀和母串的后缀对齐就是,把子串的比较位置放到前后缀最大匹配长度的位置上,如果这之前有可以匹配的答案,那么最起码这个被我们错过的答案要满足上次失败位置之前的所有字符串都匹配,而我们失败的时候和母串也成功匹配了这一部分字符串,那不是恰恰说明了这一部分前后缀相同吗,而且位置还在被我们错过的位置上,说明他的长度比最长匹配长度还要长,这不恰好与我们说这是正确答案矛盾吗,所以这种跳跃不会错过正确答案就得以证实。
第二个问题其实也是学习kmp算法中不可或缺的一部分,就是子串next数组的生成,这个next数组代表,当前下标位置的前缀字符串(不包括当前位置字符)的最长前后缀匹配长度(不能是整个前缀字符串长度,因为整个字符串前后缀必然相等)。说完了含义我们来想一想如何快速生成,其实这是一个动态规划问题,已经知道位置i及其之前的答案,能不能借助这些已知的值快速求出i+1位置的值呢,不难想到如果i位置的值恰好和之前得到的最长匹配前后缀的下一个位置匹配,恰好可以让这个长度加一就是答案,但如果不匹配应该怎么做呢,那么不难想到退而求其次,最长匹配我们不能满足,但可以尝试满足一些其他的前后缀匹配,但是应该怎么去找呢,为了不错过最长的匹配,我们应该寻找第二长的匹配尝试匹配,依次类推第三第四,直到可以成功匹配或者完全无法匹配,最长匹配长度就是0,第二长的匹配长度自然就是最长匹配结束的位置的最长匹配长度,因为因为最长匹配的匹配前后缀的完全相同,他们的最长匹配也相同,所以前缀的前后缀匹配且相同,同样与后缀的前后缀匹配且相同,这样就找到了第二长的匹配长度,依次类推可以轻松找到第三长的匹配长度,所以总结说来其实生成next数组的过程就是,首先对于前两个位置我们可以直接填好,第一个位置前面没有字符串所以是非法位置我们填入-1,第二个位置前面就一个字符,而我们不能包括整个字符串,所以填入0,然后我们可以进行动态规划直到最后,再循环中把当前位置的前一个位置(因为前一个位置才是新加入前缀字符串的),和最长匹配,第二长...依次比较,如果有匹配成功的就把这个长度加一作为当前位置答案,如果一直到0都没成功就说明没有答案,直接填入0,需要提一句的是,我们需要一个变量代表我们要比较的下标,随着next下标对应位置向下跳就可以实现最长,第二长... 的效果,一直到子串的最后。
至此基本完成了kmp 的总结,做一道板子题加深一下理解
洛谷p3375 kmp
#include<bits/stdc++.h>
using namespace std;
int nnext[1000005];
void toNext(string s){
int n=s.size();
nnext[0]=-1;
if(n==1){
return;
}
nnext[1]=0;
int tar=0;
int i=2;
while(i<=n){
if(s[i-1]==s[tar]){
nnext[i++]=++tar;
}else{
if(tar>0){
tar=nnext[tar];
}else{
nnext[i++]=0;
}
}
}
return;
}
void kmp(string s1,string s2){
int x=0,y=0;
int a=s1.size(),b=s2.size();
while(x<a){
if(s1[x]==s2[y]){
if(y==b-1){
cout<<x-y+1<<endl;
y=nnext[y];
}else x++,y++;
}else{
if(y!=0){
y=nnext[y];
}else{
x++;
}
}
}
}
void solve(){
string s1,s2;
cin>>s1>>s2;
toNext(s2);
kmp(s1,s2);
int j=s2.size();
for(int i=1;i<=j;i++){
cout<<nnext[i]<<" ";
}
cout<<endl;
}
int main()
{
solve();
return 0;
}
manacher算法
manacher算法是用来解决回文数组的问题,首先理解最粗暴简单的解,枚举每个中心朝两边扩展直到无法扩展停止,这是最粗暴的n^2 的算法,有一个简化研究问题过程的小技巧,由于我们枚举的是中心点,但是如果数组长度是一个偶数,那么实际上中心是空的,所以我们在每两个字符之间插入一个无关的字符,这样就可以实现枚举每一个中心点。
主要过程的理解需要有c代表最远回文半径对应的中心,r代表最远回文半径的位置,我们维护一个ans数组代表以以i位置为中心的最长回文半径存在ans[i]中,随后当我们来到当前的位置i
当i<r的时候:
1.如果i+ans[2*c-i]<r ans[i]=ans[2*c-i]
2.如果i+ans[2*c-i]>r ans[i]=r-i
当i=r的时候
从r的位置向后扩展直到无法扩展为ans[i] 的答案
当i>r的时候,暴力扩展即可
原理分析:
manacher的主要原理其实类似kmp,就是通过已经判断的部分看看是不是可以节省掉一些判断的可能,从而减小复杂度,当i<r的时候,其实已经研究过这个区间,我们可以看关于区间对称点2*c-i的最长回文半径的信息,因为其实关于c的两侧且处于r范围以内的是完全对称的,如果i+ans[2*c-i]<r
那么当前位置如果在想向外拓展,由于有对称点的前车之鉴,后面的位置也研究过,肯定无法配对才得到了当前的答案,这是第一种情况,如果对称点的对称区间到了最远半径之外,说明当前点到最远半径的距离就是答案,因为如果我们的答案在最远半径之外,考虑和对称点的完全对称关系,那么其实c对应的最远半径还可以往外扩展,所以很显然是因为下一个位置不能配对,所以答案就是r-i。
至于恰好落到边界r上的情况,到r之前的部分由于对称关系很明显完全配对,但我们无法判断接下来的位置是否匹配,所以我们需要往后继续拓展,i在r外我们无法得到任何有效信息,因而直接暴力扩展。
看板子代码
洛谷P3806
#include<bits/stdc++.h>
using namespace std;
void solve(){
string s;
cin>>s;
int len=s.size();
string a(2*len+1,'*');
for(int i=0;i<len;i++){
//a[i*2+1]='*';//因为要枚举中间点,但是长度是偶数的字符串中心点没办法枚举
a[i*2+1]=s[i];
}
// cout<<a;
vector<int> ans(2*len+1);
int c=0,r=0,maxx=0;//中心,最大回文半径
for(int i=0;i<2*len+1;i++){
int x=i<r?min(r-i,ans[2*c-i]):1;
while(i+x<2*len+1&&i-x>=0&&a[i-x]==a[i+x]){
x++;
}
ans[i]=x;
if(i+x>r){
r=i+x;
c=i;
}
maxx=max(maxx,ans[i]);
}
cout<<maxx-1;
}
int main(){
solve();
return 0;
}
扩展kmp
扩展kmp主要是用来解决母串a和子串b,生成一个数组e,e[i]代表a从i位置开始和b从头开始的最长公共前缀,如果我们找到一个最长公共前缀恰好是b的长度的时候,也就恰好实现了kmp问题,
所以这种算法也叫做扩展kmp,但是其具体实现几乎和manacher一模一样。
首先我们要生成一个辅助数组z,代表子串自己某下标开始和自己的最长公共前缀,我们同样需要一个c,r,含义和manacher 的一样,不同的点在于,我们在manacher中作为参照的对称点,在扩展kmp中其实是i-c的位置(i是当前位置),原理理解同样类似,其实就是i-c位置的最长公共前缀,对于当前从c开始到r的区段,我们在判断一个在r范围内的i的最长公共前缀时,可以利用i-c位置已经判断过的部分,这一部分一定匹配,同理,如果这一部分最长没有到r之外,说明下一个位置一定不匹配,否则之前的i-c位置的最长公共前缀一定更长,如果最长超过了r,那么说明最长公共前缀一定直到r位置,因为如果在长,那c的最长匹配不可能只到r,而是还可以向外延申,其余原理均和manacher异曲同工。
那么我们得到了辅助数组z,怎么最终生成我们想要解决问题的数组e呢,其实流程几乎和生成z数组的流程一模一样,只是需要注意的是,在向外延申的循环中,我们必须加上延申长度不能超过子串长度的条件,这一点很容易疏漏,因为其实很可能出现循环的情况,就是比对完整个子串接下来的位置仍然配对,从原理上应该继续延申,但是实际上从问题的含义来看最长公共前缀不可能大于子串长度,因此需要加上这个判断条件,这一点及其容易疏漏,需要注意。
其余的循环几乎和z如出一辙,其中比对的“对称点”其实是当前点i相对c的偏移相同的点,也就是z[i-c],原理的角度和z理解一样,我们利用已经比对的最长公共前缀来省去一些无谓的比较。
这就是扩展kmp的全貌。
洛谷P5410
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll min(ll a,ll b){
return a>b?b:a;
}
void solve(){
string a,b;
cin>>a>>b;
ll n=a.size(),m=b.size();
vector<ll> z(m),p(n);
ll c=1,r=1;
z[0]=m;
for(int i=1;i<m;i++){
ll len=i<r?min(r-i,z[i-c]):0;
while(i+len<m&&b[i+len]==b[len]){
len++;
}
if(i+len>r){
r=i+len;
c=i;
}
z[i]=len;
}
c=0,r=0;
for(int i=0;i<n;i++){
ll len=i<r?min(r-i,z[i-c]):0;
while(i+len<n&&len<m&&a[i+len]==b[len]){
len++;
}
if(i+len>r){
r=i+len;
c=i;
}
p[i]=len;
}
c=z[0]+1;
for(int i=1;i<m;i++){
c=c^((z[i]+1)*(i+1));
//cout<<p[i]<<endl;
}
r=p[0]+1;
for(int i=1;i<n;i++){
r=r^((p[i]+1)*(i+1));
//cout<<p[i]<<endl;
}
cout<<c<<endl<<r<<endl;
}
int main(){
solve();
return 0;
}