字符串 扩展kmp

1.1 引例

扩展kmp,求解如下问题:

问s串与t串的每一个后缀的最长公共前缀

容易发现:当某一个最长公共前缀等于s串的长度的时候,其实就是一个s串与t串的kmp匹配问题,因此得名“扩展”kmp。事实上,扩展kmp与普通kmp有一定的区别,两者并不是完全是拓扑关系。即使完全没学过kmp,直接上手学扩展kmp也不是天方夜谭

2.1 强化条件:当s与t完全相同时

先尝试通过强化条件,降低一下难度:假如s与t完全相同,扩展kmp该如何实现?

当s与t完全相同,其实问题可以进一步转化为“s与s的每一个后缀的最长公共前缀”。为了方便记录,规定:\(nxt[i]\)表示 s[1..n]与s[i..n]的最长公共前缀长度。

从最浅显易懂的地方入手。nxt[1]=s串长度,这一点我们从定义便可知;nxt[2]则暴力地枚举一遍,\(O(n)\)解决

上述部分的代码

nxt[1]=lenb;
int now=0;
while(now+2<=lenb&&b[now+1]==b[now+2]) now++;
nxt[2]=now;

考虑如何转移,扩展kmp是一个线性算法,因此当我们求解nxt[k]时,nxt[i]\((1\leq i < i)\)已经得到了求解。我们设i+nxt[i]-1的最大值为p,此时的i坐标为p0

e7Fm5t.png

记K在nxt[p0]中对应的位置,具体而言k-p0+1,=a,另L=nxt[a]

e7k9ds.png

当a+l-1< P时

e7ky6S.png

根据nxt[p0]和nxt[a]的定义,得知所有的绿色区域都是相同的。nxt[p0]的定义可知,两个红色框相同;而nxt[a]的定义可知,蓝框与红框不相同,因此无法继续匹配下去。此时:nxt[k]=nxt[a]

当a+1-l>= P时

e7VJyT.png

此时由于超出了P的范围,对于P以后的区间的信息是一无所知的,因此此时只有nxt[k]\(\geq\)P-k+1 之后我们需要暴力地枚举下去,直到失配

代码如下:

lor(i,3,lenb){
	if(i-1+nxt[i-p+1]<p+nxt[p]-1) nxt[i]=nxt[i-p+1]; // 情况一
	else{                                            // 情况二
		int now=max(0,p+nxt[p]-i);  //这是为了防止P小于k的情况
		while(i+now<=lenb&&b[1+now]==b[i+now]) now++;
		nxt[i]=now; p=i;  // 暴力枚举后,更新答案
	}
}

至此,我们得到了强化条件版:s对于s的所有后缀的最长公共前缀求解

3.1 回归原条件:当s与t不等的时候

为了区分,我们将这种情况下的匹配记录为extend[i],extend[i]表示s与t[i..n]的最长公共前缀的长度

其实,当我们分析完nxt[]数组的求解之后,会发现原问题已经在无形中被解决了

暴力地预处理extend[1]:

int now=0;
while(1+now<=lena&&1+now<=lenb&&a[1+now]==b[1+now]) now++;
extend[1]=now; p=1;

模仿nxt[]去分类讨论,只是这时候上方s[]的坐标p的含义变更为:p0+extend[p0]-1的最大值

e7ZIUJ.png

e7ZxVe.png

两种分类讨论其实无非就是把原先的两个nxt数组的利用,改为T[]的extend和s[]的nxt。代码部分也是略微修改即可。为了避免累赘本人太懒直接上代码

lor(i,2,lena){
	if(i-1+nxt[i-p+1]<p+extend[p]-1) extend[i]=nxt[i-p+1];  // 情况一
	else{                                                   // 情况二
		int now=max(0,p+extend[p]-i);
		while(1+now<=lenb&&i+now<=lena&&b[1+now]==a[i+now]) now++;
		extend[i]=now; p=i;
	}
}

4.1 总结

虽然后缀数组可以取代扩展kmp,但是如同树状数组和线段树的关系,扩展kmp有着常数小,容易写(虽然我觉得后缀数组更好写)的优点。

此外经过实测\(10^6\)级随机数据,发现速度从快到慢为:mp> kmp> ex_kmp ...

这..可能就是强大的代价吧 ( _ _)ノ|