很久以前就学过KMP,不过一直没有深入理解只是背代码,今天总结一下KMP算法来加深印象。
一、KMP算法介绍
- KMP解决的问题:给你两个字符串A和B(|A|=n,|B|=m,n>m),询问一个字符串在另一个字符串中的每一次出现位置。
- 暴力:枚举长串中的每一个起点,然后一位一位判断是否与短串完全相同,枚举复杂度是O(n),比较的复杂度是O(m),总的时间复杂度是O(nm),时间复杂度比较差
- 引入两个定义:
1.匹配串(A):被匹配的长串。
2.模式串(B):在匹配串中每次找出现位置的短串。
- 在匹配的过程中,我们相当于是要每次找到匹配串的一个前缀的后缀与模式串的一个前缀完全相同,也就是说,我们需要在匹配串上维护一个指针i,在模式串上维护一个指针j,使得Ai-j+1~i = B1~j 。
举个例子:匹配串为abbabab,模式串为abbaa,当A[i]==B[j]时直接往后继续找,当i=5,j=5时会产生失配,这时按照我们暴力的想法,我们会将i退回的到2位置,将j退回到1的位置,然后重复此过程,然而我们考虑一下就会发现这一段是一个字符也匹配不上的,因为A1~5已经和B1~5匹配上了,如果A2~6可以和B1~5匹配,则说明A1~5与A2~6完全相同,然而显然不同,于是我们有了新的思路,当我们失配时,我们缩小j,直到A的以i为结尾的前缀的后缀与B的长度为j的前缀完全相同,也就是说找到一个最大的k使得Ai-k+1~i=B1~j(k<j)。那么如何快速找到k呢?
- 这里我们引入next数组,next[j]表示模式串中长度为j-1的前缀中最长的前缀等于后缀的长度,那么当A[i]与B[j]失配时j需要往前移j-next[j]位,显然next[1]=0。
- 如何求出next数组?自己与自己匹配,如果可以匹配上,即Bnext[j-1]+1=Bj,那么next[j]=next[j-1]+1,如果不能匹配上,就让next往回跳直到可以匹配。(如果不理解可以自行画图模拟,很容易就可以理解。)
- KMP算法的大致思路就介绍完了,就是先让模式串自己与自己匹配求出next数组,然后用next数组辅助与匹配串匹配。时间复杂度为O(N+M)。
代码(洛谷P3375):
#include<bits/stdc++.h> using namespace std; const int MAXX=1e6+10; char s1[MAXX],s2[MAXX]; int l1,l2,next[MAXX]; //next[i]表示s2中以i为结尾的非前缀子串与A的前缀能够匹配的最大长度 void KMP() { for(int i=1,j=0;i<l2;i++){ while(j&&s2[i]!=s2[j]) j=next[j];//匹配不到就往下找 if(s2[i]==s2[j])j++; next[i+1]=j; } for(int i=0,j=0;i<l1;i++){ while(j&&s1[i]!=s2[j])j=next[j]; if(s1[i]==s2[j])j++; if(j==l2){ //找到一次出现 printf("%d ",i-l2+2); } } } int main() { cin>>s1;cin>>s2; l1=strlen(s1);l2=strlen(s2); KMP(); for(int i=1;i<=l2;i++) printf("%d ",next[i]); return 0; }
二、KMP小应用
- 1.(口胡题意)给你一个长度为n的字符串S,求它的最小循环节,n<=100000。
x是S的循环节的等价条件是S1~n-x+1=Sx+1~n,最小化x相当于最大化n-x+1,也就是求出next[n],然后n-next[n]几位答案。代码略。
- 2.CF126B Password(https://www.luogu.org/problemnew/show/CF126B)
一句话题意:给你一个字符串S(|S|<=1000000),找到既是S前缀又是S后缀又在S中间出现过(既不是S前缀又不是S后缀)的最长子串。
既是前缀又是后缀的最长子串很容易找,直接用上面的KMP求出next数组,next[n]即为答案,而中间出现过的子串就相当于一个前缀的后缀,所以我们只需找到不等于n的next[i]中最大的i,然后让next[n]不断往前跳直到他的长度小于前面的最大长度,这是找到这个位置往后的长度个字符构成的字符串就是答案,如果在寻找的过程中next[n]跳到0还没有找到说明无解。
代码:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; char a[1000010]; int next[1000010],n,maxx=0; void cal()//求出next数组 { next[1]=0; for(int i=2,j=0;i<=n;i++){ while(j&&a[i]!=a[j+1])j=next[j]; if(a[i]==a[j+1])j++; next[i]=j; if(i!=n)maxx=max(next[i],maxx); //找到next数组的最大值 } } int main() { cin>>a+1; n=strlen(a+1); cal(); int x=next[n]; if(x==0)printf("Just a legend "); else{ while(x>maxx)x=next[x]; //找到小于max{next[1~n-1]}的最大匹配长度 if(x==0){ printf("Just a legend "); return 0; } for(int i=2;i<n;i++) if(x==next[i]){ for(int j=i-next[i]+1;j<=i;j++) //i-next[i]+1为答案子串的左端点 printf("%c",a[j]); printf(" "); return 0; } } return 0; }