算法详解
KMP是一种字符串匹配算法,可以在线性的时间复杂度内解决字符串的“模式定位”问题,即:
在字符串 \(A\) 中查找字符串 \(B\) 出现的所有位置。
我们称 \(A\) 为主串,\(B\) 为模式串。下文都用\(n\)表示\(A\)的长度,\(m\)表示\(B\)的长度,下标从\(1\)开始。
初始状态,我们用两个指针 \(i,j\) 分别指向 \(A\) 和 \(B\) 的第 \(1\) 位。
KMP的主过程如下(后面会证明它的正确性):
- 如果\(A[i]=B[j]\),则
i++
,j++
。 - 如果\(A[i]\neq B[j]\):
- 如果\(j>1\),则
j=nxt[j-1]+1
。 - 如果\(j\le 1\),则
i++
。
- 如果\(j>1\),则
我们定义\(nxt[0]=0\)。
其中,\(nxt\)数组是一个用于表示发生失配后,\(j\)回退到的位置。具体求法待会会说,下面我们用一个例子来理解\(nxt\)的功能,我将\(nxt\)数组画在图片的下方了。
- 初始状态。\(i=1,j=1\)。
- \(A[i]=B[j]\),同时右移\(i,j\)。\(i=2,j=2\)。
- \(A[i]=B[j]\),同时右移\(i,j\)。\(i=3,j=3\)。
- \(A[i]=B[j]\),同时右移\(i,j\)。\(i=4,j=4\)。
- \(A[i]=B[j]\),同时右移\(i,j\)。\(i=5,j=5\)。
- \(A[i]=B[j]\),同时右移\(i,j\)。\(i=6,j=6\)。
- 发现\(A[i]\neq B[j]\),于是查表\(nxt[j-1]+1=nxt[5]+1=3\),于是\(j=3\)。此时\(i=6\)。
可以发现此时灰色箭头指向的两部分是相等的。
进一步来说,从上一张图开始,下划线的部分都是相等的。
- OK我们继续,\(A[i]=B[j]\),于是\(i,j\)同时右移,\(i=7,j=4\)。
- 发现\(A[i]\neq B[j]\),查表得\(nxt[3]+1=2\),于是\(j=2\)。此时\(i=7\)。
我们又发现\(A\)与\(B\)的下划线部分重合了,所以仍然可以从重合部分之后开始比较。
- 然后\(A[i]=B[j]\)……就一直比到结束了,就不放图了w。
我们可以把根据上面的过程(KMP主过程)写出代码(下标从\(1\)开始):
i=1,j=1;
while(i<=n){if(a[i]==b[j]) i++,j++;else if(j>1) j=nxt[j-1]+1;else i++;if(j==m+1) cout<<i-j+1<<"\n";//输出配对的开始位置,理解一下这里
}
可以发现\(i\)是始终不降的,比起暴力,KMP通过避免\(i\)指针的回滚操作,减少了时间开销。
到这里我们已经可以初步理解\(nxt\)的含义,它是用来帮我们了解发生失配时,\(j\)应该回退到什么位置。
再进一步分析,我们发现:\(nxt[x]\),其实就是\(B[1\sim x]\)这一段的最长公共前后缀长度。
举个例子,YABAPQYABZABAAPYABAPQYABA。这是一个长度为\(24\)的字符串,那么当它作为\(B\)时:
- \(nxt[23]=9\),如你所见,红色部分是\(B[1\sim 23]\)的公共前后缀,它的长度为\(9\)。
- \(nxt[9]=3\),因为
YAB
是\(B[1\sim 9]\)的公共前后缀,它的长度为\(3\)。 - \(nxt[3]=0\),因为\(B[1\sim 3]\)没有公共前后缀。
- \(nxt[24]=0\),因为\(B[1\sim 24]\)没有公共前后缀。
注意:公共前后缀可能有重叠部分,但是一个字符串不能作为它自己的公共前后缀(严格来说,我们把这种前/后缀称作真前/后缀,所以后面都带“真”来称呼了)。
我们规定,\(nxt[x]\)表示的必须是\(B[1\sim x]\)这一段的最长公共真前后缀长度,这么定义是为了保证正确性,如果不规定最长,不能保证答案的正确性。后面的证明会提到。
现在我们的首要问题是,如何求\(nxt\)数组?
我们假定\(i\)之前的答案都已经求出,现在需要求\(nxt[i]\)。
我们设\(j\)为\(nxt[i-1]+1\),如下图:
最简单的情况自然就是\(B[i]=B[j]\),那么\(nxt[i]=j\),这是显然的。
但是也可能遇到更棘手的情况:
\(B[i]\neq B[j]\),怎么办呢?
既然YABAPQYAB这个公共真前后缀不行,那就再找一个更小的!
显然更小的前后缀就是YAB了,怎么得来的呢?就是看\(nxt[j-1]\)的值是多少,那么新的\(j\)就是\(nxt[j-1]+1\)。此时再看看\(B[i]=B[j]\)是否成立。成立的话就\(nxt[i]=j\),不成立就继续找\(j\),直到找到\(B[i]=B[j]\),或者\(j=1\)为止。
可以发现我们充分利用了之前计算出的\(nxt\),将原规模逐步缩小,是不是有点归纳法的意味?
再来举一个不断更新\(j\)直到\(j=1\)为止的例子:
我们可以把这一步骤也写成代码(下标从\(1\)开始):
int i=2,j=1;//nxt[1]应为0,所以i从2开始
while(i<=m){if(b[i]==b[j]) nxt[i++]=j++;else if(j>1) j=nxt[j-1]+1;else nxt[i++]=0;
}
(是不是和上面的代码很像)
至此把两个代码拼接起来就能得到KMP的全过程了。
P3375 【模板】KMP
下标从$1$开始
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N];
int main(){cin>>a>>b;n=a.size(),m=b.size();a=' '+a,b=' '+b;int i=2,j=1;while(i<=m){if(b[i]==b[j]) nxt[i++]=j++;else if(j>1) j=nxt[j-1]+1;else nxt[i++]=0;}i=1,j=1;while(i<=n){if(a[i]==b[j]) i++,j++;else if(j>1) j=nxt[j-1]+1;else i++;if(j==m+1) cout<<i-j+1<<"\n";}for(int i=1;i<=m;i++) cout<<nxt[i]<<" ";return 0;
}
下标从$0$开始
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N];
int main(){cin>>a>>b;n=a.size(),m=b.size();int i=1,j=0;while(i<m){if(b[j]==b[i]) nxt[i++]=++j;else if(j) j=nxt[j-1];else nxt[i++]=0;}i=0,j=0;while(i<n){if(a[i]==b[j]) i++,j++;else if(j) j=nxt[j-1];else i++;if(j==m) cout<<i-j+1<<"\n";}for(int i=0;i<m;i++) cout<<nxt[i]<<" ";return 0;
}
正确性证明
我们刚接触\(nxt\)的定义时,可能会有疑问:为什么\(nxt\)一定要表示\(B[1\sim x]\)这一段的最长公共真前后缀长度?
我们用反证法证明一下正确性,顺带解决这个问题。
根据KMP的流程,如果遇到失配的情况,我们先找\(B[1\sim (j-1)]\)中的最长公共真前后缀(如图,用\(f\)表示)的长度\(nxt[j-1]\),然后把\(j\)置为\(nxt[j-1]+1\),如下图。
要证明KMP是正确的,就是证明\(j\)回退时不会跳过正确答案。
我们假设在\(j>nxt[j-1]+1\)时存在匹配,如下图,那么灰色箭头连接的部分都相等。
自然,黄色大括号部分(不包含\(i,j\))也相等,那么黄色部分的长度\(=j-1\)。
回到第\(1\)张图(\(j'\)表示第\(1\)张图情况下的\(j\)),根据假设,\(j>|f|+1\),即黄色部分长度\(>|f|\)。
这样的话,黄色部分就成了\(B[1\sim j']\)最长的公共真前后缀,与“\(f\)是\(B[1\sim j']\)最长公共真前后缀”矛盾。
得到的结论是:\(j>nxt[j-1]+1\)时一定不存在匹配。
也就是说\(j\)回退是不会漏掉解的,正确性得证。
这样我们的问题也迎刃而解了:如果\(nxt\)不是最长,上面的情况就可能不形成矛盾,进而\(j>nxt[j-1]+1\)时也可能存在解,而这被我们忽略掉了,所以正确性不能保证。
复杂度证明
通过看代码能发现两部分代码相似,所以就一块说了:
\(i\)每增加\(1\),\(j\)最多也增加\(1\),从而\(j\)最多增加\(len\)次,进而最多减少\(len\)次。
所以处理\(nxt\)是\(O(m)\)的,主过程是\(O(n)\)的,总时间复杂度就是\(O(n+m)\)了。——字符串学习笔记 · 浅析KMP——单模式串匹配算法 - 皎月半洒花
\(\mathcal{NEXT\ \ \ PHANTASM...}\)