前言
首先,kmp算法大家都知道,但是听到扩展kmp的时候,会想到底是干什么的?
那么扩展kmp算法是用来求解下面问题的:
给定母串S,和子串T。
定义 n=|S| , m=|T| , extend[i]=S[i..n] 与T的最长公共前缀长度。请在线性的时间复杂度内,求出所有的 extend[1..n] 。
我们其实可以知道,如果有某个位置i满足
extend[i]=m
,那么T就肯定在S中出现过,并且进一步知道出现首位置是i——而这正是经典的KMP问题。
因此可见“扩展的KMP问题”是对经典KMP问题的一个扩充和加难。
算法描述
在解上面这个问题前我们要先解决一个类似的问题:求字符串s的所有后缀和s本身的最长公共前缀;
我们用next[]数组保存这些值;
现在我们假设要求
next[x]
,并且
next[i]0<i<x
的值都已经求出;
我们设
p=k+next[k]−1
, k是使p最大的i(
0<i<x
);如图:
现在整理一下所解问题:
已知:
s[k..p]==s[0..next[k]−1]
,求
s[x..n−1]
与
s[0..n−1]
的最长公共前缀;
由
s[k..p]==s[0..next[k]−1]
得:
s[x..p]==s[x−k..next[k]−1]
———1//这个是显然的
并设
L1=p−x+1
;
因为
x−k
肯定是小于x的所以
L2=next[x−k]
是已知的,得:
s[0..L2−1]==s[x−k..x−k+L2−1]
; ——–2
通过等式1,2可以推出
s[0..k1]==s[x..k2]
当
L1≤L2
如下图
此时表示
s[0..L1−1]==s[x..x+L1−1]
但不能确定蓝色部分是否相等,所以需要继续比下去
当
L1>L2
如下图:
此时表示
s[0..L2−1]==s[x..x+L2−1]
而且因为
L2=next[x−k]
使得
s[L2]!=s[x+L2]
所以
next[x]=L2
;
回到原来的问题
假设此时已经求出
next[]
,我们用
extend[]
保存字符串S的所有后缀和字符串T的最长公共前缀的值
我们重复上面的过程:
现在我们假设要求
extend[x]
,并且
extend[i]0<i<x
的值都已经求出;
我们设
p=k+extend[k]−1
, k是使p最大的 i (
0<i<x
);如图:
现在整理一下问题:
已知:
s[k..p]==T[0..extend[k]−1]
,求
s[x..n−1]
与
T[0..m−1]
的最长公共前缀;
由
s[k..p]==T[0..extend[k]−1]
得:
s[x..p]==T[x−k..extend[k]−1]
———1//同样这个是显然的
并设
L1=p−x+1
;
因为
x−k
肯定是小于x的所以
L2=next[x−k]
是已知的,得:
T[0..L2−1]==T[x−k..x−k+L2−1];
——–2
通过等式1,2可以推出
T[0..k1]==s[x..k2]
当
L1≤L2
如下图
表示
T[0..L1−1]==s[x..x+L1−1]
但不能确定蓝色部分是否相等,所以需要继续比下去
当
L1>L2
如下图:
表示
T[0..L2−1]==s[x..x+L2−1]
而且因为
L2=extend[x−k]
使得
T[L2]!=s[x+L2]
所以
extend[x]=L2
;
如果你仔细阅读的话可能会发现并没有说 next[] 怎么求,我们发现计算next实际上以T为母串、T为子串的一个特殊“扩展的KMP”。具体可以参考标准KMP的求法就行了。
复杂度分析
容易看出,在计算的过程中,凡是访问过的点,都不需要重新访问了。一旦比较,都是比较以前从不曾探访过的点开始。因此总的时间复杂度是 O(n+m) ,是线性的。
应用例题
这里有一道例题可以练手:HDU4333
题意:给一个数字,每一次把它的最后一位拿到最前面,一直那样下去,分别求形成的数字小于,等于和大于原来数的个数。
例如:134可以形成134,341,413三个数,所以分别是1,1,1。
数据范围:50组以内数据,数字大小在 10100000 以内。
由于长度为len的字符串形成题目要求的串的个数为len,那么我们可以把原来的两个串T连接起来形成字符串S,然后找S的每个后缀的前len个元素即可。
这里主要是如何比较的问题,对于字符串的比较,我们可以先求出他们的最长公共前缀长度,然后只需要比较一次就可以知道结果了。那么最长公共前缀怎么求,由于这里是一个串T与另一个串S,来求S的所有后缀与T的最长公共前缀长度,所以用扩展KMP。如果
extend[i]≥len
,就说明与原来的相等了,否则如果
S[i+extend[i]]<T[extend[i]]
就说明小于,否则就是大于。
另外还推荐一道题目POJ2185。
代码
代码是上面的例题的代码,同时中间的函数也可以当成模板来使用。
#include <cstdio>
#include <cstring>
#include <algorithm>
const int size = 500000+10;
int next[size],nextval[size],extend[size];
char s[size],t[size];
void Getnext(char *T) {
int a=0,Tlen=strlen(T);
next[0]=Tlen;
while(a<Tlen-1&&T[a]==T[a+1]) a++;
next[1]=a;a=1;
for(int k=2;k<Tlen;k++) {
int p=a+next[a]-1,L=next[k-a];
if((k-1)+L>=p) {
int j=(p-k+1)>0?p-k+1:0;
while(k+j<Tlen&&T[k+j]==T[j]) j++;
next[k]=j;a=k;
}
else next[k]=L;
}
}
void Getextend(char *s,char *t) {
int a=0;Getnext(t);
int Slen=strlen(s),Tlen=strlen(t);
int Minlen=std::min(Slen,Tlen);
while(a<Minlen&&s[a]==t[a]) a++;
extend[0]=a;a=0;
for(int k=1;k<Slen;k++) {
int p=a+extend[a]-1,L=next[k-a];
if((k-1)+L>=p) {
int j=(p-k+1)>0?p-k+1:0;
while(k+j<Slen&&j<Tlen&&s[k+j]==t[j]) j++;
extend[k]=j;a=k;
}
else extend[k]=L;
}
}
void Nextval(char *t) {
int i=0,j=-1;nextval[0]=-1;
int Tlen=strlen(t);
while(i<Tlen) {
if(j==-1||t[i]==t[j]) {
i++;j++;if(t[i]!=t[j]) nextval[i]=j;
else nextval[i]=nextval[j];
}
else j=nextval[j];
}
}
int Slen,Tlen;
int main()
{
int Slen,Tlen,T=1,tt=1;
scanf("%d",&T);
while(T--) {
scanf("%s",s);strcpy(t,s);
strcat(s,t);Getextend(s,t);
Tlen=strlen(t);Slen=strlen(s);
Nextval(t);
int MOD=Tlen-nextval[Tlen],temp=1;
if(Tlen%MOD==0) temp=Tlen/MOD;
int ans1=0,ans2=0,ans3=0;
for(int i=0;i<Tlen;i++) {
if(extend[i]>=Tlen) ans2++;
else if(s[i+extend[i]]<t[extend[i]]) ans1++;
else ans3++;
}
printf("Case %d: %d %d %d\n",tt++,ans1/temp,ans2/temp,ans3/temp);
}
return 0;
}
最后
针对字符串处理的许多算法,我们考虑的时候能够尽量保证:已经访问过的点绝不再访问,充分利用已经得到的信息。,这也是扩展kmp算法的魅力之所在。