序文
このブログの文字列のインデックスは1ベースです。
導入
与えられた二つの文字列\(A、B \) 、尋ねる(\ B)\かどうか\(A \)ストリング。
これらの問題のために、我々はより多くの暴力のアイデアを持っている、つまり、それをペアリングするために少しずつ。
コードを考えます:
int Check(){
for(int i=1;i+M-1<=N;i++){
int j=0;
while(j<M&&A[i+j]==B[j+1])j++;
if(j==M)return 1;
}
return 0;
}
しかし、明らかに、このアルゴリズムは非常に良いではありません。(カードであってもよいと\(O(N \ CDOT M)\) )
(ほとんどの質問のために十分であるが)
KMPアルゴリズムの導入。
P1(定義)
以下のためにA=ababababc
、B=ababc
たとえば、私たちは見てCheck
処理機能。
最初のステップは、2つの文字列が対になってもよいabab
ことが遭遇するまで、(緑側)a
とc
ペア(赤側)が見つかりません。4つのステップの寄与をここに。
第二のステップは、再び二つの文字列とペアabab
、そして、再び4段階の寄与を組み合わせることができません。
第3のステップと、正確溶液に一致する二つの文字列、および再度4つのステップの貢献。
注:上記のトラバースに加え寄与の数を意味するA
文字列内のステップの数。
上記のプロセスの最適化を考えてみましょう。
私たちの最初のステップの不一致が、図のケースに直接ジャンプすることができたときには、見つけることができます。
それが私たちですj
から、完全に下付き4
ジャンプ2
(不一致前の添え字)。
私たちが上扱うことができれば、であるB
私達は同じように操作をジャンプすることができるように、文字列配列。
その結果、我々は定義\(次\)の配列を:
\(次[I] \)を表す\(B \ [1]) 〜\(B [I] \)これは、文字列内のすべてのサフィックスサブストリングが接頭辞であります接頭辞に登場するインデックス文字列内で最も長い文字列の末尾。(限界値ではない\(I \)、すなわちセルに前進する少なくともジャンプ動作)。
(言語は良くありません、それは例を見た方が良いです)
それとも:(前にその一例B=ababc
)
、その後に処理\(次[4] \) (すなわち、サブストリングabab
)、私たちの暴力のコースがあるとしては、次のとおりです。
すべての接尾サブストリング(自分自身ではない)を識別します。
その後、見つけますすべての接頭辞(も自分ではない):
発見されたab
、すなわち、文字列の中で最も長い文字列を検索するために二回発生していると、そのための要件を満たす\(次[4] \)上の値(2 \)\。
この例では(B=ababc
)すべてのために\(次\)の値である:
次のようにジャンプの不一致があります:
P2(初期化)
だから、どのように見つける必要があります(\次)\配列それ?
考虑求解\(Next[i]\)时(假设\(Next[1]\)~\(Next[i-1]\)都求好了),我们如何用之前的状态转移。设之前的状态长这样:紫点与绿点是完全相等的两个子串,弧线表示\(Next[i-1]\),我们现在要求红点的\(Next\)值。
那么我们只需要去比较一下下图的红点与橙点是否相等就行了:
如果相等,那么 \(Next[i]\) 就等于 \(Next[i-1]+1\) 。
否则,我们就去访问下标为 \(Next[Next[i]]+1\) 的点再次比较,直到不能比较为止。
因为这样的话,同样也满足\(Next\)数组的性质:
\(B[i-1]=B[Next[i-1]]=B[Next[Next[i-1]]]=...=B[Next[....]]\)
而最终求得的那个前缀,同样会是\(B[1]\)~\(B[i]\)的某个后缀。
P3(求解)
基于P1与P2的内容,P3就比较好理解了。
我们在最初那个暴力Check
上改改就行了。
如果失配了,回到一个满足条件的Next[j]
就行了。
原理呢,其实和P2初始化部分是一样的。
实在不懂的话,那还是举个例子吧。
对于A=ababababc
,B=ababc
时,我们用KMP算法来做一下。
发现a
和c
失配,现在\(j=4\),考虑让\(j=Next[j]\),更改后\(j=2\),贡献为4。
继续往后,发现失配,现在\(j=4\),再次让\(j=Next[j]\),更改后\(j=2\),贡献为3。
情况变化成下图:
最后出解,贡献为3。
虽然只比暴力的总贡献少两步,但在某些恶意卡暴力的题中,还是得用KMP算法。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=1000005;
int K,N,M,Next[MAXN],Ans;
char A[MAXN],B[MAXN];
void Prepare(){
for(int i=2;i<=M;i++){//注意从2开始.
int j=Next[i-1];
if(B[j+1]!=B[i]&&j>0)j=Next[j];
if(B[i]==B[j+1])j++;//判等于0的情况.
Next[i]=j;
}
}
int Find(){
int i=1,j=0;
for(int i=1,j=0;i<=N;i++){
while(j&&A[i]!=B[j+1])j=Next[j];
if(A[i]==B[j+1])j++;
if(j==M){
Ans++;
j=Next[j];
}
}
}
int main(){
scanf("%d",&K);
while(K--){
scanf("%s%s",B+1,A+1);
N=strlen(A+1);M=strlen(B+1);
Ans=0;Prepare();Find();
printf("%d\n",Ans);
}
}
/*
ababababc
ababc
*/
后记
关于KMP算法的时间复杂度:
Q:求解\(Next\)时,\(j\)指针难道不会跳很多次吗,这个复杂度难道不是\(O(M^2)\)吗?
A:其实可以发现,每次j=Next[j]
的操作都会使当前的\(Next\)值比上一次的\(Next\)值小。
画出\(Next\)的函数图像如下:
那么对于满足\(Next[i]>Next[i-1]\)的\(Next[i]\)肯定都是在\(O(1)\)的时间里出解的。
而那些\(Next[i]<Next[i-1]\)的\(Next[i]\)总跳跃次数并不会超过\(M\)。
因为\(Next\)函数的值域是在\(M\)以内的,而且一次上升的差距肯定为1。
(即若\(Next[i]>Next[i-1]\),那么有\(Next[i]=Next[i-1]+1\))
而一次回跳至少跳1格,故总回跳次数是不会超过\(M\)次的。
Q:求解时每次失配,\(j\)指针最多还是会跳\(M\)次嘛,看似还是可以卡到\(O(N\cdot M)\)嘛。
A:实则不然,指针\(j\)每次都是和\(i\)一起变化的,只有在\(i\)加1时,\(j\)才有可能跟着加1。这样的话,\(j\)失配往回跳的总次数是不会超过\(N\)次的(每次跳都至少跳1格)。故KMP算法的时间复杂度是十分优秀的\(O(N)+O(M)=O(N+M)\)啦。