KMP模式匹配
一、何谓模式串匹配
模式串匹配,就是给定一个需要处理的文本串(理论上应该很长)和一个需要在文本串中搜索的模式串(理论上长度应该远小于文本串),查询在该文本串中,给出的模式串的出现有无、次数、位置等。
模式串匹配的意义在于,如果我是一个平台的管理员,我可以针对一篇文章或者一句话,搜索其中某个特定脏字或者不雅词汇的出现次数、位置——次数可以帮助我决定采取何种等级对于该用户的惩罚方式,而位置则可以帮助我给每一个脏词打上“*”的标记来自动屏蔽这些脏词。
二、浅析 $KMP$ 之思想
首先要理解,朴素的单模式串匹配大概就是枚举每一个文本串元素,然后从这一位开始不断向后比较,每次比较失败之后都要从头开始重新比对,大概期望时间复杂度在 $Θ(n+m)$ 左右,对于一般的弱数据还是阔以跑的了滴。但是其实是可以被卡成$O(nm)$的。$emmmm$并且还是比较容易卡的。
而 $KMP$ 的精髓在于,对于每次失配之后,我都不会从头重新开始枚举,而是根据我已经得知的数据,从某个特定的位置开始匹配;而对于模式串的每一位,都有唯一的“特定变化位置”,这个在失配之后的特定变化位置可以帮助我们利用已有的数据不用从头匹配,从而节约时间。
比如我们考虑一组样例:
模式串:abcab
文本串:abcacababcab
首先,前四位按位匹配成功,遇到第五位不同,而这时,我们选择将$abcab$向右移三位,或者可以直接理解为移动到模式串中与失配字符相同的那一位。可以简单地理解为,我们将两个已经遍历过的模式串字符重合,导致我们可以不用一位一位地移动,而是根据相同的字符来实现快速移动。
但有时不光只会有单个字符重复:
模式串:abcabc
文本串:abcabdababcabc
当我们发现在第六位失配时,我们可以将模式串的第一二位移动到第四五位,因为它们相同 $qwq$ .
模式串: abcabc
文本串:abcabdababcabc
那么现在已经很明了了, $KMP$ 的重头戏就在于用失配数组来确定当某一位失配时,我们可以将前一位跳跃到之前匹配过的某一位。而此处有几个先决条件需要理解:
1、我们的失配数组应当建立在模式串意义下,而不是文本串意义下。 因为显然模式串要更加灵活,在失配后换位时,更灵活简便地处理。
2、如何确定位置呢?
首先我们要明白,基于先决条件$1$而言,我们在预处理时应当考虑当 模式串 的第 $i$ 位失配时,应当跳转到哪里.因为在文本串中,之前匹配过的所有字符已经没有用了——都是匹配完成或者已经失配的,所以我们的 $kmp$ 数组(即是用于确定失配后变化位置的数组,下同)应当记录的是:
在模式串 $str1$ 中,对于每一位 $str1(i)$ ,它的 $kmp$ 数组应当是记录一个位置 $j$, $j≤i$ 并且满足 $str1(i)=str1(j)$ 并且在 $j!=1$ 时理应满足 $str1(1)$ 至 $ str1(j−1) $ 分别与 $str(i−j+1) \sim\ str1(i−1)$ 按位相等
上述即为移位法则
3、从前缀后缀来解释 $KMP$ : 首先解释前后缀(因为太简单就不解释了 $qwq$ ):
给定串:ABCABA
前缀:A,AB,ABC,ABCA,ABCAB,ABCABA
后缀:A,BA,ABA,CABA,BCABA,ABCABA
其实刚才的移位法则就是对于模式串的每个前缀而言,用 $kmp$ 数组记录到它为止的模式串前缀的真前缀和真后缀最大相同的位置(注意,这个地方没有写错,是真的有嵌套 $qwq$ )。然而这个地方我们要考虑“模式串前缀的前缀和后缀最大相同的位置”原因在于,我们需要用到 $kmp$ 数组换位时,当且仅当未完全匹配。所以我们的操作只是针对模式串的前缀−−−−毕竟是失配函数,失配之后只有可能是某个部分前缀需要“快速移动”。所以这就可以解释 $KMP$ 中前后缀应用的一个特点:
$KMP$中前后缀不包括模式串本身,即只考虑真前缀和真后缀,因为模式串本身需要整体考虑,当且仅当匹配完整个串之后;而匹配完整个串不就完成匹配了吗 $qwq$
三、代码实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+50;
char s1[N],s2[N];
int n,m,nxt[N],f[N];
signed main(){
scanf("%s",(s1+1));
getchar();
scanf("%s",(s2+1));
n=strlen(s1+1),m=strlen(s2+1);
nxt[1]=0;
for(int i=2,j=0;i<=m;i++){
while(j>0&&s2[i]!=s2[j+1])j=nxt[j];
if(s2[i]==s2[j+1])j++;
nxt[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j>0&&(j==m||s1[i]!=s2[j+1]))j=nxt[j];
if(s1[i]==s2[j+1])j++;
f[i]=j;
if(j==m)printf("%lld\n",i-j+1),j=nxt[j];
}
for(int i=1;i<=m;i++)printf("%lld ",nxt[i]);
return 0;
}