最近学习了一下 KMP 算法,写一下总结,免得忘了。
KMP算法:
暴力匹配的浪费:
假设正在进行下图这样的匹配 :
暴力匹配一旦匹配失败,模式串就会回退到开头进行匹配;
我们可以看到,失配字符 ‘C’ 的前两个字符 (AB) 和开头 (AB) 是一样的,如果挪到开头去匹配,很明显会进行一些不必要的匹配;
显然,将箭头所指字符与失配处进行匹配才是 比较 优的。
KMP 算法就是基于这个思想的。只要知道有前缀与后缀匹配 的 最大长度,那某个位置失配之后,就能迅速的像上面那样跳到一个 比较 优的位置进行匹配。
为什么是 “比较优” ?后面再说。
什么叫 “前缀和后缀匹配” ?
对于一个位置 $i$ ,如果 $str[0, k - 1] = str[i - k, i - 1] $,那么就说 $i$ 位置前缀匹配后缀的最大长度为 $k$ ;我们用 $Next$ 数组存起来,即 $Next[i] = k$ ;
那么,对 $Next[]$ 下个定义:
$\bullet ; Next[i] : \quad str[0, i - 1]$ 中前缀匹配后缀的最大长度。
如果我们计算出了模式串 $P$ 的每个 $Next[i]$ ,那么在 $pos$ 位置处失配时,立马就可以跳到 $Next[pos]$ 处进行匹配;
所以,$Next$ 数组还可以这样理解:
$\bullet ; Next[i] : \quad$ 当 $i$ 位置失配时应该跳往的匹配位置。
比如上面的模式串 $P$ :
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
P | A | B | D | C | A | B | C | ‘\0’ |
Next | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
$\quad i = 0$,对于模式串的首字符,我们统一为 $Next[0] = -1$;
$\quad i = 1$,前面的字符串为 $A$,其最长相同真前后缀长度为 $0$,即 $Next[1] = 0$;
$\quad i = 2$,前面的字符串为 $AB$,其最长相同真前后缀长度为 $0$,即 $Next[2] = 0$;
$\quad i = 3$,前面的字符串为 $ABD$,其最长相同真前后缀长度为 $0$,即 $Next[3] = 0$;
$\quad i = 4$,前面的字符串为 $ABDC$,其最长相同真前后缀长度为 $0$,即 $Next[4] = 0$;
$\quad i = 5$,前面的字符串为 $\color{red}{A}BDC\color{red}{A}$,其最长相同真前后缀为 $A$,即 $Next[5] = 1$;
$\quad i = 6$,前面的字符串为 $\color{red}{AB}DC\color{red}{AB}$,其最长相同真前后缀为 $AB$,即 $Next[6] = 2$;
$\quad i = 7$,前面的字符串为 $ABDCABC$,其最长相同真前后缀长度为 $0$,即 $Next[7] = 0$。
还是开始那个例子,当 $i = 6$ 时,失配,立马就可以跳到 $i = 2$ 的位置进行匹配; faster了有没有?
如何求Next数组?
假设我们现在正在计算 $Next[i]$ ,即之前的 $Next[0, \ldots , i-1]$ 已经得到了;
先上代码:
1 | void Get_Next(string P) { |
代码中最重要的就是那个 if…else… 语句;
一脸懵逼???不怕,上图:
假设 $i$ 和 $p$ 的位置如上图,上一次前后缀匹配的最大长度为 $p-1$,即绿色的两个椭圆是相等的;
(1) if (P[i] == P[p]) ** ,则往后走就是了;
$\qquad$ p == -1 又是为什么呢?一是 p 初始化为-1,总得往后走吧!二是当前后缀匹配长度为 0 时,p会为-1,这时候也得往后走;
(2) **else
由 $Next[p]$ 的定义可知,前两个绿色的椭圆是相等的;于是可知第 $1$ 个和第 $4$ 个椭圆是相等的;
因此,令 $p=Next[p]$ 来加速匹配;
完整代码:
这其实是一道题:HDU-2087
1 | /********************************************** |
KMP优化:
上面还有一个问题没有解决,为什么说 “比较优” ?
问题所在:
其实一开始那个表格就能看到问题了,但还是看个”严重”点的吧!
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
P | A | B | A | C | D | A | B | A | C | E | ‘\0’ |
Next | -1 | 0 | 0 | 1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
假设 $i=8$ 时失配,按照之前的KMP算法,就会把 $p=3$ 处的字符拿过来匹配;然而,这两个字符是相同的,就增加了一些不必要的匹配;
稍微修改一下代码就能解决这个问题:
1 | void Get_Nextval(string P) { //KMP优化 |
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
P | A | B | A | C | D | A | B | A | C | E | ‘\0’ |
Next | -1 | 0 | 0 | 1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
Nextval | -1 | 0 | -1 | 1 | 0 | -1 | 0 | -1 | 1 | 4 | 0 |
优化之后,当 $i=8$ 失配时,就能直接跳到 $p=1$ 处匹配,faster 了有没有?匹配方式还是没变的;
严格来讲,未优化的 KMP 算法叫 MP算法;但一般情况下,未优化的 KMP 算法已经够用了;
KMP算法(未优化): $Next[]$ 表示前后缀匹配的最大长度,还有一些骚操作,后面再说;
KMP算法(优化): $Nextval[]$ 表示最优长度,但不一定是最长;快是快了,功能少了;
复杂度:$O(n+m)$ 并不会证明
More about KMP :
KMP求最小循环节:
现在假设 $i$ 位置之前的字符串是循环的(比如 $str[0, i-1] = aabaab$) ,我们想求它的最小循环节;
根据 $Next[]$ 的定义,如果 $i$ 位置 相匹配的前后缀没有相互覆盖,显然这个前缀一定不会是循环的,所以我们主要考虑前后缀相互覆盖的情况;
如图所示,$Next[i]$ 为红色那段,然后将后面剩下的那段黑色的定义为 $loop$ ,即 $loop = i - Next[i]$;
由于串是在自匹配,所以 $loop$ 是等于后面的两段的;
由 $Next[]$ 定义可知,前两段绿线隔出来的串是相等的,后两段绿线隔出来的串也是相等的;
那我们把前后两段都砍了;
这和上面的是一样的嘛!继续砍。。。
砍到最后,剩下的 $4$ 个串都是相等的;自然就可以推出上一次砍掉的 $4$ 个串也是相等的;
有点类似于递归回溯的思想;
那么,就说明 $loop$ 就是 $i$ 以前的前缀的 循环节;
再来说为什么是最小的?很简单,
$Next[]$ 的定义是什么?前后缀匹配的 最大 长度!显然,剩下的 $loop$ 就是最小的啦!
既然我们能将每个位置以前的最小循环节求出,那整个串的最小循环节自然就不是问题了;
有一道题可以做一下:HDU-1358
题意就是让从小到大输出哪些位置之前的串是循环的,以及循环次数;
参考代码:
1 | /******************************************** |