浅说KMP算法
KMP算法
字符串算法的引入
字符串算法是信息学竞赛中比较重要的一个模块,在普及组,提高组,省选,\(NOI\) 比赛中均有出现。
比较简单的题主要考察字符串模拟,稍微复杂一点的题就会涉及到一些字符串算法,一些更难的题还会用到字符串算法加其他算法,比如字符串加动态规划,字符串+数据结构等等。同时,无论是字符串算法还是其他算法,都是对思维的锻炼,在很多地方都有共通之处,很多时候也会结合在一起。比如有的时候,虽然题目中是数字,但是要转换成字符串来处理,有的题目虽然是字符串,但是要转换成数字来处理。
字符串算法中比较常见的有字符串匹配和回文串。字符串匹配分为单串匹配和多串匹配,单串匹配指判断一个字符串是否是另一个字符串的字串,一个字符串长度为 \(n\),另一个字符串长度为 \(m\),暴力匹配的最坏时间复杂度为 \(O(n\times m)\),但是可以通过 KMP 算法或者字符串 \(hash\) 优化到 \(O(n+m)\),多串匹配可以用字典树或者 \(AC\) 自动机解决。
回文串指判断一个字符串是否是回文串,或者求最长回文子串,求子回文串暴力做法最坏复杂度是 \(O(n \times n)\),但是可以通过 \(maxnache\),后缀数组等算法优化到 \(O(n)\),也可以用比较简单的二分加 \(hash\) 优化到 \(O(n\log n)\)。
KMP
KMP做为一大经典算法,功能非常的单一,只有匹配字符串的这一用途,但是非常非常的快,而传统的暴力匹配的时间复杂度又过于的高,所以说还是非常有必要学一学的,实在不行,也可以当作DP来学对吧。(我当初就是抱着这个心态学的)
我们先来看一下传统的暴力匹配是怎么做的:
\(\begin{cases} A=a b a b a b a c \\ B=a b a c \end{cases} \rightarrow \begin{cases} A=a b a b a b a c \\ B=\,\ a b a c \end{cases}...\rightarrow \begin{cases} A=a b a b a b a c \\ B=\;\;\; \,\ \ \ a b a c \end{cases}\)
显然我们这个是一步一步的暴力的,当B不能匹配的时候,就暴力往下挪一,然后继续匹配(如上图)。
但是这样会有很多点是无用的,那么我们就可以在这些位置进行优化操作。我们可以发现,如果说 \(abab\) 匹配不了的话,它的下一个 \(baba\) 一定是匹配不了的,为什么?因为字符串 \(B=abac\) 并没有一个前缀等于 \(bab\),也就是说没有可能使 \(bab\) 为 \(abac\) 的开头,那么这个点就是无用的。但是我们现在可以发现 \(a\) 是可以作为 \(abac\) 的前缀的,那么我们其实是可以直接跳到 \(abab\) 的。
那么这个 \(a\) 有什么特殊的性质吗?可以很显然的发现,\(a\) 即是第一个 \(abab\) 的后缀,也是 \(abac\) 的前缀,而且还是以 \(a\) 结尾的关于字符串 \(B\) 的最长的公共前后缀。那么我们的切入口就找到了。
我们现在的思路就变成了:暴力向后扩展,如果匹配到一个点(记为 \(i\)),但是这个点的下一位匹配不了的时候,我们就应该找以 \(i\) 为结尾的一个最长的字符串 \(s\),并保证 \(s\) 为字符串 \(B\) 的前缀。我们还是拿上面的那个例子来说:
\(\begin{cases} A=a b a b a b a c \\ B=a b a c \end{cases}\)
现在 \(aba\) 可以匹配,但是 \(c\) 匹配不了,所以说就应该找 \(a\) 的最长公共前后缀,并移动。
\(\begin{cases} A=a b a b a b a c \\ B=\,\,\,\,\,a b a c \end{cases}\)
现在 \(aba\) 可以匹配,但是 \(c\) 又匹配不了了,所以说还要找 \(a\) 的最长公共前后缀,并移动。
\(\begin{cases} A=a b a b a b a c \\ B=\,\,\,\,\,\, \,\, \, \, \, a b a c \end{cases}\)
现在就可以匹配了。
但是我们很明显可以发现,一次的挪动可能是不行的,换句话说,就是你挪到了当前结尾字符的最长公共前后缀的位置,可能还是匹配不了!这又怎么办呢?
这其实也很好想,我们设 \(i\) 表示当前 \(A\) 字符串匹配到的位置, \(j\) 表示当前的结尾字符的位置的下一位,并且 \(i\) 与 \(j\) 匹配不了,同时,我们设 \(P[i]\) 表示以 \(i\) 结尾的字符串的最长公共前后缀的下一位。那么我们就有以下推导过程:
若 \(a[i]\neq b[j]\),则 \(j=P[j-1]\)。
若 \(a[i]=b[j]\),则 \(j+1\),并且暂停寻找,因为我们已经找到了。
这个过程可能有点抽象,但是模拟一下就可以了,实在不行,看代码吧。
int j=0;//代表当前为的下一位(即j-1可以匹配,但是j不能匹配
int P[INF];//p[i]代表以i结尾的最长公共前后缀
for (int i=0;i<a.length();i++){
while (j>0&&a[i]!=b[j])j=P[j-1];
if (a[i]==b[j])j++;
if (j==b.length()){
//代表找到一个
}
}
那么我们现在的目光就应该放到如何处理 \(P\) 数组了。我们回顾一下 \(P\) 数组的定义——\(P[i]\) 表示以 \(i\) 结尾的字符串的最长公共前后缀。这个是不是很像自己去匹配自己?我们可以稍微推导一下:
令 \(A=ababcab\),同时我们假设已经知道了 \(P[0]=0,P[1]=0,P[2]=1\) ,现在要求 \(P[3]\) 的值,怎么求呢?我们要分类讨论:
设当前点为 \(i\),要匹配的点为 \(j\)。
若 \(B[i]=B[j]\),则 \(j+1\),并且 \(P[i]=j\)。
若 \(B[i]\ne B[j]\),则 \(j=P[j-1]\),直到 \(A[i]=B[j]\) 或 \(j=0\),然后此时 \(P[i]=j\)。
重复这两次的操作,我们就可以得到正确的答案。
不难发现,这个过程和上面所讲的KMP的匹配过程是非常相似的,其实这也就是刚才所说的自我匹配。
P[0]=0;
int j=0;
for (int i=0;i<b.length();i++){
while (j>0&&b[i]!=b[j])j=P[j-1];
if (b[i]==b[j])j++;
P[i]=j;
}
非常显然,在匹配过程中 \(A,B\) 都没有回退,所以时间复杂度为 \(\cal O(n+m)\)。
例题1
这道题都是叫做模板了,那么肯定可以直接写,而题目中的 \(border\),其实就是我们求的 \(P\) 数组。
#include<bits/stdc++.h>
using namespace std;
const int INF=1e6+10;
string a,b;
int P[INF];
void prepare(){
int len=b.length();
P[0]=0;
int j=0;
for (int i=1;i<len;i++){
while (j&&b[i]!=b[j])j=P[j-1];
if (b[i]==b[j])j++;
P[i]=j;
}
}
void KMP(){
prepare();
int lena=a.length(),lenb=b.length();
int j=0;
for (int i=0;i<lena;i++){
while (j&&a[i]!=b[j])j=P[j-1];
if (a[i]==b[j])j++;
if (j==lenb){
cout<<i-lenb+1+1<<endl;//还要加1,题目中要求下标从1开始
}
}
}
int main(){
cin>>a>>b;
KMP();
int len=b.length();
for (int i=0;i<len;i++){
cout<<P[i]<<" ";
}
return 0;
}
KMP的应用
KMP可以检查一个字符串是否由多个相同的字符串组合而成
我们还是考虑 \(P\) 数组的意义,\(P\) 数组求得是最长公共前后缀,而 \(P[n]\) 则是以 \(n\) 结尾的最长公共前后缀的位置的下一位,那么就有 \(A[n]=A[P[n]-1]\),这是必然的。又因为是有循环节的,所以说循环就一定是从 \(A[P[n]-1]\) 到 \(A[n]\) 的。这个是可以理解的吧。
同时我们可以注意到,这个逻辑在当前字符串为一个多个字符串组合而成的字符串中截取一段的情况也是可以使用的。具体的,可以自己证明一下。
例题2
信息学奥赛一本通(C++版)在线评测系统——Power String
这道题就是上面的很裸的模拟,我们可以直接写:
#include<bits/stdc++.h>
using namespace std;
const int INF=1e6+10;
int P[INF];
string a;
void prepare(){
int len=a.length(),j=0;
P[0]=0;
for (int i=1;i<len;i++){
while (j>0&&a[i]!=a[j])j=P[j-1];
if (a[i]==a[j])j++;
P[i]=j;
}
}
void KMP(){
prepare();
int n=a.length();
if (n%(n-P[n-1])==0)cout<<n/(n-P[n-1])<<"\n";
else cout<<1<<"\n";
}
int main(){
while (1){
cin>>a;
int qlen=a.length();
if (qlen==1&&a==".")return 0;
KMP();
}
return 0;
}

浙公网安备 33010602011771号