KMP算法
\(KMP\)算法
\(KMP\)字符串匹配([题目][1])
内容:给出两个字符串\(s_1、s_2\),求出\(s_1\)在\(s_2\)中出现的位置
做法:
1.先举个例子,设\(s_1="ABBABBABABAA"s_2="ABBABBABABAAABABA"\)
2. 那么一个最朴素的算法就诞生了
将\(s_2\)与\(s_1\)一位一位进行对比,但我们发现这个时间复杂度为\(O(nm)\)
3. 那该如何优化它呢,我们发现对于一次匹配后,下一次可能出现的位置一定是要前几位相同的,那我们假设当前匹配到了第\(6\)位,也就是说前\(6\)位是相同的,那下一个会可以匹配的可能会出现在哪里呢?
4.其实就是要寻找最长相同前后缀,对于\(ABBABB\)来说(假设匹配成功),那么它的下一个可以成功匹配的可能是在第\(4\)位\(A\)开始(如图
),就可以跳过中间无用的匹配
5.如何证明其中没有跳过可能成功匹配的子串呢?
我们假设\(ABC\)为最长相同前后缀,那么如果中间还有一个串\(AB\)与\(ABC\)的头能成功匹配,假设\(...=C+...\),那很明显最长前后缀为\(ABC...ABC\),矛盾,舍,若\(...\not=C+...\),明显\(AB\)处不能匹配,所以下一个可能匹配成功的位置一定在最长相同前后缀的后缀的开始处。(如果没有,那我们就只好往下暴力找了)
6.那该如何寻找最长相同前后缀呢?其实我们发现找最长相同前后缀是不需要\(s_2\),因为要跟\(s_1\)匹配,则是\(s_1\)在移动,就相当于是要在\(s_1\)中,找到下一个匹配的位置。我们设\(kmp[i]\)表示当前\(i\)位匹配成功,第\(i+1\)位匹配失败,下一位从\(s1[kmp[i]+1]\)开始找,也可以理解为\(kmp[i]\)是以\(s1[i]\)开头,以\(s1[i]\)结尾的最长公共前后缀的长度!
7.求\(kmp\)的过程,我们设一个\(k\),表示当前匹配到第\(k\)位,让后打一个\(1\)~\(s1.length()-1\)的循环,对于当前位的下一位,若是可以匹配,那就\(k++,kmp[i+1]=k\),否则就根据不匹配的情况回到\(kmp[k]\)重新比较,直到\(k=0\)或者\(s1[i+1]==b[k+1]\),若是\(k=0且s1[i+1]!=b[k+1]\),\(kmp[i+1]=0\)
8.最后在按照匹配方法把\(s_2\)跑一遍,若是匹配到了\(s_1\)的最后一位,那就输出位置,且将\(k=kmp[k]\)(回去),时间复杂度为\(O(n+m)\)
代码实现
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
void read(int &sum)
{
sum=0;char last='w',ch=getchar();
while (ch<'0' || ch>'9') last=ch,ch=getchar();
while (ch>='0' && ch<='9') sum=sum*10+ch-'0',ch=getchar();
if (last=='-') sum=-sum;
}
char s1[1000001],s2[1000001];
int kmp[1000001],k;
int lena,lenb;
int main()
{
// freopen("M.in","r",stdin);
// freopen("M.out","w",stdout);
scanf("%s",s2+1),lenb=strlen(s2+1);
scanf("%s",s1+1),lena=strlen(s1+1);
k=0;
for (int i=1;i<lena;i++)
{
while (k!=0 && s1[i+1]!=s1[k+1]) k=kmp[k];
if (s1[i+1]==s1[k+1]) kmp[i+1]=k+1,k++;
}
k=0;
for (int i=0;i<lenb;i++)
{
while (k!=0 && s2[i+1]!=s1[k+1]) k=kmp[k];
if (s2[i+1]==s1[k+1]) k++;
if (k==lena) printf("%d\n",i-lena+2),k=kmp[k];
}
for (int i=1;i<=lena;i++) printf("%d ",kmp[i]);
// fclose(stdin);fclose(stdout);
return 0;
}
扩展\(KMP\)([题目][6])
内容:给出字符串\(s_1、s_2\),求出每个\(s_1[i...s_1.length]\)与\(s_2\)的最长公共前缀
做法:
- 根据\(KMP\)字符串匹配的思想,我们设\(kmp[i]\)表示以\(i\)为首和开头以为首的最长公共前缀,我们知道可以先\(s_2\)自己与自己匹配一遍
- 我们先定义一个\(k\)表示最近一位匹配到哪了,跟\(KMP\)字符串匹配类似,设\(p=k+kmp[k]-1\),\(L=kmp[i-k+1]\)于是就有了(动图)
(相同颜色代表相同区间)- 这时候我们要分类讨论一下\(i-k+L\)和\(p-k+1\)的大小了
\(1).i-k+L<p-k+1\)
如图,明显的\((i...p)=(i-k+1...p-k+1)\),\(1...L=i-k+1...i-k+L\),因为绿段是最长的了,所以\(kmp[i]=L=kmp[i-k+1]\)
\(2).i-k+L>=p-k+1\)
如图,明显的最长为\(ip\)但我们不是知道\(ip\)后面是否有更长的,于是就要暴力枚举
- 最后处理与\(s_2\)的关系,与\(KMP\)匹配类似,走一遍即可
- 注意开头几项要暴力,因为前面没有,还有\(p-i+1\)有可能小于\(0\),要改为\(0\),防止下标越界
代码实现
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define int long long
using namespace std;
typedef long long ll;
void read(int &sum)
{
sum=0;char last='w',ch=getchar();
while (ch<'0' || ch>'9') last=ch,ch=getchar();
while (ch>='0' && ch<='9') sum=sum*10+ch-'0',ch=getchar();
if (last=='-') sum=-sum;
}
char s1[2*10000001],s2[2*10000001];
int kmp[2*10000001],ex[2*10000001],k;
int lena,lenb;
void EX_KMP()
{
kmp[1]=lena;
k=1;
while (s1[k]==s1[k+1]) k++;k--;kmp[2]=k;
k=2;
for (int i=3;i<=lena;i++)
{
int p=k+kmp[k]-1,L=kmp[i-k+1];
if (i-k+L<p-k+1) kmp[i]=L;
else
{
int j=p-i+1;
if (j<0) j=0;
while (s1[i+j-1+1]==s1[j+1] && i+j-1+1<=lena) j++;
kmp[i]=j;
k=i;
}
}
k=1;
while (s1[k]==s2[k] && k<=lena && k<=lenb) k++;k--,ex[1]=k;
k=1;
for (int i=2;i<=lenb;i++)
{
int p=k+ex[k]-1,L=kmp[i-k+1];
if (i-k+L<p-k+1) ex[i]=L;
else
{
int j=p-i+1;
if (j<0) j=0;
while (s2[i+j-1+1]==s1[j+1] && i+j-1+1<=lenb && j+1<=lena) j++;
ex[i]=j;
k=i;
}
}
}
signed main()
{
// freopen("M.in","r",stdin);
// freopen("M.out","w",stdout);
scanf("%s",s2+1),lenb=strlen(s2+1);
scanf("%s",s1+1),lena=strlen(s1+1);
EX_KMP();
int ans=0;
for (int i=1;i<=lena;i++) ans=ans^(i*(kmp[i]+1));
printf("%lld\n",ans);
ans=0;
for (int i=1;i<=lenb;i++) ans=ans^(i*(ex[i]+1));
printf("%lld\n",ans);
// for (int i=1;i<=lenb;i++) printf("%d ",ex[i]);
// fclose(stdin);fclose(stdout);
return 0;
}
浙公网安备 33010602011771号