Upd on 2023.5.14:重述本篇文章部分内容,使得文章更加易懂。
前言
在我们今天的学习之前,常见的字符串算法除了 STL 之外就是哈希。但哈希有一定的错误概率,如果想要 100%100\%100% 正确,就要双哈希,非常麻烦。那么,有没有一些算法,保证正确性的同时还拥有优秀的复杂度呢?
这就是今天的主角:KMP 算法。
本文默认字符串下标从 111 开始。
算法讲解
KMP 是由三位大佬:D.E.Knuth\mathcal{D.E.Knuth}D.E.Knuth,J.H.Morris\mathcal{J.H.Morris}J.H.Morris,V.R.Pratt\mathcal{V.R.Pratt}V.R.Pratt,取他们名字首字母,就是 K,M,PK,M,PK,M,P。
原理
KMP 常用于解决单串匹配问题。即给定两个字符串 sss 和 ttt,问 ttt 在 sss 中的出现情况(出现次数或出现位置等)。
我们将 sss 称为文本串,ttt 称为模式串。
一般的解法是对于 sss 的每个位置都放一个 ttt 上去一位一位匹配。这种算法效率底下的原因是因为某些位置可能判断过很多次。
而 KMP 的优越性在于,每次匹配失败后,不会重头再来,而是根据之前匹配的“经验”跳到下一个可能可以成功匹配的地方,从而减少冗余信息的计算。
我们以下面的例子介绍:
现在文本串的第 555 位失配(匹配失败)了。KMP 会将模式串对齐至文本串的第 333 位。继续匹配,变成这样:
为什么他会跳到第 333 位?
与很多人理解的不同,它跳到第 333 位的原因是因为模式串本身的字符结构,而不是文本串。因为现在是第 555 位失配,证明前 444 位都已匹配成功。
而模式串的一二位与三四位是相等的,文本串的三四位又与模式串三四位是匹配的,所以直接挪到文本串第三位,可以保证一二位相等。
换言之,如果我们能知道 t1∼it_{1\sim i}t1∼i 的最后若干位与前面若干位相同,那么在 iii 失配之后就可以直接跳过这么多位,从而大大增加效率。
用公式表示,我们对每一个 iii,求一个 kmpikmp_ikmpi,使得 t1∼kmpit_{1\sim kmp_i}t1∼kmpi 和 ti−kmpi+1∼it_{i-kmp_i+1\sim i}ti−kmpi+1∼i 这两段是相同的,即前后缀相同的长度。
但是这个方法不够完美。比如在 mmmmmmmmmmmmmmmmh
中查找 mmmmmms
,你会发现每一次都是到最后一位才失配,又要跳回第一位重新匹配,比较恶心人。
所以我们要求的是最长长度而不是任意长度。
那么,问题是,如何求 kmpkmpkmp 数组?
考虑使用一种类似递推的方式。对于一个位置 iii,如果 ti=tkmpi−1+1t_{i}=t_{kmp_{i-1}+1}ti=tkmpi−1+1,那么我们发现一定有 kmpi=kmpi−1+1kmp_i=kmp_{i-1}+1kmpi=kmpi−1+1,如图:
其实就是在 kmpi−1kmp_{i-1}kmpi−1 的基础上加上了上图中两个蓝色的部分。
那如果不相同呢?我们还要往前跳,那又要跳到哪里呢?
不能随便跳,因为 ti=tkmpi−1+1t_{i}=t_{kmp_{i-1}+1}ti=tkmpi−1+1 的前提是 ti−1=tkmpi−1t_{i-1}=t_{kmp_{i-1}}ti−1=tkmpi−1。所以我们考虑再次将这个递推式进化,变成:ti−1=tkmpi−1=tkmpkmpi−1t_{i-1}=t_{kmp_{i-1}}=t_{kmp_{kmp_{i-1}}}ti−1=tkmpi−1=tkmpkmpi−1。那我们就可以重复上述的过程,不停的判断是否相等,如果不相等就继续嵌套。
如图,匹配失败后,红色框往它的 kmpkmpkmp 值走了。走完发现这回相等了,于是 kmpi=kmpkmpi−1+1kmp_i=kmp_{kmp_{i-1}}+1kmpi=kmpkmpi−1+1,即蓝色部分。
这样的时间复杂度是对的,至于怎么分析,要用到均摊,作者比较菜,故省略。
那么边界是多少呢?显然 kmp0=0kmp_0=0kmp0=0。问题是 kmp1kmp_1kmp1 是多少?按定义来说,前后缀长度相同的长度应该是 111 呀!
但是这样就有问题了。比如有一个 iii,它的 kmpkmpkmp 跳着跳着来到了 111。此时应该比较的是 tit_iti 和 tkmp1+1=t2t_{kmp_1+1}=t_{2}tkmp1+1=t2。如果不相等,又跳回到 kmpkmp1=kmp1=1kmp_{kmp_1}=kmp_{1}=1kmpkmp1=kmp1=1,你就会发现它出不来了。
有人说,加个特判不行吗?不好意思,不行。你并不知道你是不是第一次进 kmp1kmp_1kmp1。如果你一到 111 就 break
那将无法匹配第一位,从而导致 kmpikmp_ikmpi 全部变成 000。
所以,为了方便,我们将 kmp1=0kmp_1=0kmp1=0,这样一到 000 我们就停手,可以保证能够正确地求出 kmpkmpkmp 数组。
void init(){
kmp[0] = kmp[1] = 0;
for(int i=2;i<=m;i++){
int j = kmp[i - 1];
while(j > 0 && t[i] != t[j + 1])
j = kmp[j];
if(t[i] == t[j + 1])
kmp[i] = j + 1;
}
}
那么,现在就是用 ttt 匹配 sss 了。其实这个过程也是相当相似的,只是我们把 ttt 与 ttt 自己匹配换成了 ttt 与 sss 匹配,所以代码也很容易:
int now = 0;//当前匹配到什么位置
for(int i=1;i<=n;i++){
while(now > 0 && s[i] != t[now + 1])
now = kmp[now];
if(s[i] == t[now + 1])
++now;
if(now == m)//匹配成功了
printf("%d\n", i - m + 1);
}
组合起来再输出 kmpkmpkmp 数组就可以过掉 P3375 啦!