发布于摸鱼世界
前置芝士
KMP字符串匹配
前言
什么是扩展KMP(exkmp)?它是KMP的升级版,能够在线性时间内快速求出一个字符串的所有后缀与这个字符串的LCP(最长公共前缀)长度。
问题引入:给定两个字符串s
与t
,分别代表文本串与模式串,下标从0开始,长度分别为n
,m
。定义一个数组extend[i]
等于s[i,n-1]
(从第i位开始的后缀)与t
的最长公共前缀的长度。(有点绕,但是一定要记住这个定义)
(这里表格挂了,需要看的可以进开头链接看)
很明显,当extend[i]
等于t
串的长度时,t
串就在s
串中出现,并且是从i
位开始的。是不是和KMP的功能有亿点类似?因此它叫做扩展KMP。
正文
先看这样一个流程。假设我们循环求解这个extend[]
数组的值,并且当前已经遍历到了i
处,需要额外记录下两个值:a和p。因为当前已经遍历到了i
,所以extend[0,i-1]
都已经完成了求解。我们需要在这些值中找到一个最大的extend[i]
,把它的值记录为p,把这个i
记录为a。换种说法,也就是p 代表以 a 为起始位置的字符匹配成功的最右边界,也就是 "p=最后一个匹配成功位置+1"。比如上面这张图中,a=0,p=5
。
把上下两段字符串进行比较,我们可以得到s[a,p)==t[0,p-a)
这个结论。
此时再引入一个辅助数组next[]
,表示t
串的每一个后缀与t
串本身的最长公共前缀的长度。注意,这里是假设,求法先暂时不管。
比如上面的串"aaaaac"
对应的next[]
的值如下表所示:
i | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
t | a | a | a | a | a | c |
next[] | 6 | 4 | 3 | 2 | 1 | 0 |
操作流程
接下来模拟一下是怎么利用已知的信息在线性的时间内求出每个数的extend[]
值的。为了更方便理解,这里就不带入实际数据。
模拟开始!
因为在对应的情况下,t
串的下标总是比s
串的下标小a
,所以我们可以得到s[i]==t[i-a]
。
然后我们在next[]
数组中寻找一个可以给我们的匹配提供信息的值。它就是next[i-a]
。
再回顾一下next[]
数组的含义。它所表达的是"在t串中 第i位开始的后缀 与 t串 的 最长公共前缀"。所以next[i-a]
的意思就是,从 第i-a
位开始的t
串 与 完整的t串 的最长公共前缀。
(1)
如图,根据定义,被橙色椭圆圈起来的三段应该是完全相同的。因为在之前的推算中,我们已经得到了s[a,p)==t[0,p-a)
,又因为根据next[]
的定义,t串中的两段应该是相等的,所以三段都相等。
next[i-a]
根据定义,表示的就是橙色那一段的长度,再加上一个i,就得到了s
串中的橙色段的末尾的位置。
如图,在这个例子中,i+next[i-a]<p
,根据定义,extend[i]=next[i-a]
。根据图可以理解这一点。橙色段就是要求的LCP。
结论1:当i+next[i-a]<p
,extend[i]=next[i-a]
(2)
在这种情况下,i+next[i-a]==p
。因为p是最右端点,所以在这里是认为它是不匹配的。并且在这里,因为i~p的长度等于next[i-a],所以p-i与next[i-a]重合。
因为有重合的情况,所以s[p]!=t[p-a]且t[p-i]!=t[p-a]
,但s[p]
与t[p-i]
不一定相等。所以我们如果要确定,直接从s[p]
与t[p-i]
开始匹配,避免了部分的重复计算过程。
至于是否正确,我们可以来分析一下。根据next[]
的定义,我们已经确定了t[0,p-i)==s[i,p)
,后面的只需要匹配一下就可以了。
(3)
可以看到,除了上面提到的两种情况,还有一种情况就是i+next[i-a]>p
在这种情况下,我们可知s[i...p)==t[i-a...p-a)
,但是s[p]!=t[p-a]
且t[p-i]==t[p-a]
。所以我们根据定义,得到extend[i]=p-i
。要问为什么,再去理解一下前面的区间相等有哪些,再反复看一下两个数组的定义,尽可能理解。
(4)
看起来解决了所有的问题,但还有一个最根本的。我们的next[]
在前面的表示中一直都只是一个假设,我们应该怎么求呢?这个时候,还是从定义出发。
expend[i] -> s[i,n-1]与t的最长公共前缀长度
next[i] -> t[i,m-1]与t的最长公共前缀长度
是不是很相似?再联系kmp中由kmp->getnext的思想,魔改一下getextend的代码,就可以求出next
数组了。特殊的,我们需要先把next[0]=m
,这样才能保证后面操作的正确性。
模板题,最后异或计算答案的时候记得把ans
设置为long long
类型即可。
代码中需要注意的地方说一说。
1.当模式串不在文本串中出现时,会出现p<=i
的情况。但是我们在假设中一直都是把p
视作大于i
的,所以如果有这样的问题,那就使p=i
就可以了。
2.为了在过程中不访问超出范围的位置,我们在进行访问之前,需要先判断下标是不是超出范围。简单的,只需要将其作为一个判断的条件放在挨个比较之前即可。但是在自匹配和匹配文本串的时候的条件是不一样的,详见代码。
3.把i+next[i-a]>p
和i+next[i-a]==p
的情况合并的原因是,等于的情况时,我们需要用while
循环求出最新的p并且更新。但是在大于的情况如果做相同的操作,并不会有任何改变,并且最终的结果都是extend[i]=p-i
,所以可以合并来降低代码实现难度。
如果还有疑问/博文描述有误,欢迎在下方评论指出。
希望这篇博文能够对你有所帮助。over.