KMP
前置概念
$Border$:如果字符串同长度的前缀与后缀相同,称该前缀(后缀)是一个$Border$(可以指这个前缀也可以指这个前缀的长度)
循环周期:对于字符串$S$和正整数$p$,如果有$S_i=S_{i-p}$,对于$p\lt i\leq |S|$成立,称$p$是$S$的一个循环周期
循环节:若字符串$S$的周期$p$满足$p\mid |S|$,称$p$是$S$的一个循环节
重要性质$1$:$p$是$S$的周期$\Leftrightarrow |S| - p$是$S$的$Border$
因此求$Border$问题与求周期问题等价
重要性质$2$:$S$的$Border$的$Border$也是$S$的$Border$
因此求$S$的所有$Border$等价于求最大$Border$(求完最大$Border$变为子问题)
$Next$数组
$nxt_i$的值为$prefix_i$的非平凡最大$Border$
记$nxt_1=0$
考虑$prefix_i$的非平凡最大$Border$,去掉最后一个字符,就是$prefix_{i-1}$的非平凡最大$Border$
但反过来,$prefix_{i-1}$的非平凡最大$Border$加上一个字符,不一定是$prefix_i$的非平凡最大$Border$($S_i$与$S_{nxt_{i-1}+1}$不一定相等)
因此想求$nxt_i$,需遍历$prefix_{i-1}$的所有非平凡$Border$,结合重要性质$2$,发现这个过程等价于去遍历$nxt_{i-1},nxt_{nxt_{i-1}},\cdots,0$
从大到小遍历,如果某一个$Border$加上后一个字符满足相等,就可以及时$break$
求$Next$数组代码
1 | void get_nxt() |
求$Next$数组复杂度分析
考虑势能分析
如果$nxt_i=nxt_{i-1}+1$,势能增加$1$
否则势能会先减少到某个$nxt_j$,然后有$nxt_i=nxt_j+1$,在寻找$nxt_j$的过程中,势能减少,每次至少减少$1$,寻找结束后势能加$1$
如果一直到$j=0$都没有找到满足的$nxt_j$,说明$nxt_i=0$,势能清空
综上,势能总量为$O(N)$,时间复杂度就是$O(N)$,常数不超过$2$
$KMP$思想
$Next$数组记录了每个位置的前缀的非平凡最大$Border$信息
利用这个信息可以加速字符串匹配
先看暴力匹配为什么慢
1 | for (int i = 1, j = 1; i <= n; ) |
暴力匹配的时间瓶颈在于:当遇到匹配失败的字符时,$i$和$j$大量的回溯
$KMP$算法首先预处理出模式串$t$的$Next$数组,然后基于这样一个事实节约时间:当遇到匹配失败的字符,$i$指针不需要动,尝试让$j$回溯到$prefix_{j-1}$所有$Border$的长度即可,非$Border$长度一定不会匹配的更“远”
“尝试让$j$回溯到$prefix_{j-1}$所有$Border$的长度”这个过程,结合重要性质$2$和已经预处理好的$Next$数组,其实还是一个在$Border$链上跳的过程
结合势能分析,复杂度为$O(N+M)$
1 | for (int i = 1, j = 1; i <= n; ) |
$KMP$模板
1 | struct KMP { |