[OI] KMP
\(\mathscr{1}\) 模式串匹配
\(\mathscr{1.1}\) 概念解释
对于一个字符串 \(S\),我们定义:
\(S\) 的前缀 \(Pre_{i}\) 表示 \(S\) 前 \(i\) 位组成的子串.
\(S\) 的后缀 \(Suf_{i}\) 表示 \(S\) 后 \(i\) 位组成的子串.
\(S\) 的真前缀 \(P\ Pre_{i}\) 表示满足 \(Pre_{i}\neq S\) 的 \(Pre_{i}\).
\(S\) 的真后缀 \(P\ Suf_{i}\) 表示满足 \(Suf_{i}\neq S\) 的 \(Suf_{i}\).
\(S\) 的 \(border\) 表示一个满足 \(s=P\ Pre_{i}\) 且 \(s=P\ Suf_{i}\) 的 \(s\).
\(border'(S)\) 表示 \(S\) 的最长 \(border\).
如字符串 AaBbCcBbAa
的 \(border'=\) Aa
. 因为它既是 \(P\ Suf_{2}\) 也是 \(P\ Pre_{2}\),且最长.
\(\mathscr{1.2}\) 前缀函数 \(\pi(x)\)
\(\pi(x)\) 是关于字符串 \(S\) 的函数. \(\pi(x)\) 定义为 \(border'(Pre_{x+1})\) 的长度
如对于 abcabcd
有
\(x\) | \(Pre_{x+1}\) | \(border'(Pre_{x+1})\) | \(\pi(x)\) |
---|---|---|---|
\(0\) | a |
- | \(0\) |
\(1\) | ab |
- | \(0\) |
\(2\) | abc |
- | \(0\) |
\(3\) | abca |
a |
\(1\) |
\(4\) | abcab |
ab |
\(2\) |
\(5\) | abcabc |
abc |
\(3\) |
\(6\) | abcabcd |
- | \(0\) |
\(\mathscr{1.3}\) 前缀函数 \(\pi(x)\) 的求法
我们可以注意到,假如我们想将 \(\pi(i)\) 的值扩展到 \(\pi(i+1)\),那么这个扩展的值最多就是 \(1\),并且只发生在当前前缀的后一个字符与后缀的前一个字符相等时. 因此我们可以利用 \(\pi(i)\) 的值直接递推到 \(\pi(i+1)\),在每次递推过程中直接判断扩展的两个字符是否相等即可.
另外,我们还可以发现,在给 \(Pre_{i}\) 加上一个 \(S[i]\) 并进行扩展的过程,其实就是对前缀的后一个字符与后缀的后一个字符(即新加入的字符)进行的比较. 因此我们实际上在递推的时候采用的是将新加入字符与当前前缀的后一个字符进行匹配的过程. 如 cacca
扩展到 caccac
,就是在当前 \(border=\) ca
的基础上匹配第三位与新加入的第六位,匹配成功后进行扩展得到的.
另外,我们还需要考虑当不相等时要如何处理. 可以发现,既然当前前缀和后缀匹配不上,我们就需要找到一个更小的 \(border\) 来进行扩展,也许会获得成功.即 caccac
扩展到 caccaca
,当 \(border=\) cac
的时候会导致最后一个 a
失配,但是当 \(border=\) c
的时候就可以成功扩展成 ca
. 因此我们失配后重新匹配的过程其实也是寻找更小的 \(border\) 的过程.
那么我们如何去寻找更小的 \(boeder\) 呢. 设 \(\pi(i)\) 对应的前缀为 \(Pre_{i}\),后缀为 \(Suf_{i}\),那么按照定义,\(Pre_{i}\) 与 \(Suf_{i}\) 应该是完全相等的. 假如我们把 \(Pre_{i}\) 单独看作一个整体,会发现其实它也有自己的一个 \(border\) ,并且长度为 \(\pi(\pi(i))\). 那么 \(Suf_{i}\) 的 \(border\) 肯定也是这个了,所以推出来 \(Pre_{i}\) 的这个前缀与 \(Suf_{i}\) 的后缀完全相等,那么它就也是原字符串的一个 \(border\). 并且根据 \(\pi(i)\) 始终最大的性质,可以知道当前这个就是最大的 \(border\),长度为 \(\pi(\pi(i))\). 那么假如这个也失配,我们就继续寻找 \(\pi(\pi(\pi(i)))\) ,以此类推.
综上,我们得出以下这一段代码:
cin>>b;
for(int i=1;i<=b.length()-1;++i){
int j=pi[i-1]; //定位到前缀后一位
while(j>0&&b[i]!=b[j]){ //失配处理
j=pi[j-1];
}
if(b[j]==b[i]) j++; //匹配处理
pi[i]=j; //赋值
}
\(\mathscr{2}\) KMP
假设我们有一个模式串 \(P\) 需要和 \(S\) 进行匹配(即找到 \(P\) 在 \(S\) 中的出现位置).
假定我们将 \(P\) 从左向右一一比对,即每次比对时,模式串在左边的情况已经全部考虑完毕.
即:
abcabcad
bcad
bcad
bcad
bcad
bcad
那么显然这样做是对的,但是会导致我们的效率很低. 效率低的主要原因在于对已经比较过的字符又反复比较. KMP 算法即是来解决这个问题的.
当我们进行一次比较时,假如能够成功匹配,那么我们可以使用朴素算法快速得出答案. 但当匹配不成功的时候,特别是只有前半部分匹配成功了的时候,我们能不能直接利用已经匹配过的前半段字符,直接跳过一部分比较. 比如:
abcabcad
bcad
在第四位失配了,那么我们能不能直接跳过之后的几次匹配 (因为我们通过这次匹配已经知道前三位必须要是 bca
才有可能匹配成功了),直接去匹配最后的 bcad
,这样效率就会快很多了.
因此我们考虑,在本次失配之后,如果想让模式串跳到下一个具有本次已匹配前缀的文本串位置,我们应该怎么做.
我们在第一节讲到的前缀数组恰好可以用于此处. 假若模式串的前几位已经匹配,那么我们找到从起点到失配位置的字符串的最大 \(border\),即 \(\pi(j)\),将模式串平移 \(\pi(j)\) 位,因为该字符串的前缀我们已经匹配过,与模式串相同,而 \(border\) 的性质使得平移后,模式串本来在前缀部分的内容恰好移动到了与之相同的后缀部分. 因此这一部分我们便无需再次匹配了.
这样的情况不仅适用于初次失配,并且适用于全部位置的失配情况. 因为每次我们移动都是基于失配前字符相等,而移动后的匹配又给这个相等添加了新的字符,即每次失配的位置都只会比前一次更靠后,因此 KMP 的速度会加快.
另外,对于 \(\pi(j)=0\) 的情况,即当前情况已经无法再继续拓展了,说明前面这些部分无法匹配成功. 我们只需要丢弃前半部分,继续在后方进行匹配即可.
动图
来源 致谢