CF1721E 题解(KMP学习笔记)
CF1721E 题解
题面
前置知识
KMP,基本的字符串函数。
(不懂 KMP 的可以翻到最底下看看后记)
思路
对于每个询问,其实只要把 \(t\) 接在 \(s\) 的后面跑 KMP 即可。(不懂 KMP 的看后记!)
但这样会 TLE。。。
很遗憾,那就多设一个数组 \(last\)。
设 \(last_{i,j}\) 表示第 \(i\) 个前缀的所有 border(最长公共前后缀)中,最长的使得下一位刚刚好是 \(j\) 的位置。
如:若 \(s_{i+1}=j\),则 \(last_{i,j}=i\),否则 \(last_{i,j}=last_{f_i,j}\)
好了,不多说了,上代码!
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int N=1000025;
char s[N];
int n,m,q,last[N][35],f[N];
void init(){
int j=0;
for(int i=2; i<=n; i++){
while(j&&s[j+1]!=s[i]) j=f[j];
f[i]=j+=s[j+1]==s[i];
}
for(int i=0; i<n; i++){
for(int j=0; j<26; j++) last[i][j]=last[f[i]][j];
last[i][s[i+1]-'a']=i;
}
}
int main(){
scanf("%s",s+1);//从1开始输入
n=strlen(s+1);//求原字符串的长度
init();//初始化
scanf("%d",&q);
while(q--){
scanf("%s",s+n+1);//从n+1开始输入,直接接在原字符串后面
m=strlen(s+n+1);//求新输入的字符串的长度
for(int i=n+1; i<=n+m; i++){
f[i]=0;
for(int j=0; j<26; j++) last[i][j]=0;
}
for(int j=0; j<26; j++) last[n][j]=last[f[n]][j];
last[n][s[n+1]-'a']=n;
int j=f[n];
for(int i=n+1; i<=n+m; i++){
if(s[j+1]!=s[i]) j=last[j][s[i]-'a'];
f[i]=j+=s[j+1]==s[i];
printf("%d ",f[i]);
for(int k=0; k<26; k++) last[i][k]=last[j][k];
if(i!=n+m) last[i][s[i+1]-'a']=i;
}
printf("\n");//别忘了换行
}
return 0;
}
后记
KMP
问题引入
算法简介
KMP 算法(Knuth-Morris-Pratt 算法)是一个字符串匹配算法。
即对于两个字符串 \(s\) 和 \(t\),长度分别为 \(n\) 和 \(m\)。我们分别称它为文本串 \(s\) 和模式串 \(t\)。
KMP 可以在 \(O(n+m)\) 求得模式串 \(t\) 在文本串 \(s\) 中的所有出现位置和出现次数。
这里 \(t\) 在 \(s\) 中出现即为 \(t\) 作为 \(s\) 的子串。
在 KMP 算法中,我们需要对于模式串先预处理出一个 \(fail\) 数组,记为 \(f\) 数组。
\(f[i]\) 表示 \(t\) 的前缀 \(t_0,t_1,\cdots,t_i\) 的 border 长度。
border 的中文意思为“最长公共前后缀”。对于某个字符串,他的 border 定义为该串最长的不为自身的前缀,使得该串存在一个后缀与这个前缀相同。
算法流程
朴素的暴力方法是枚举 \(s\) 的每个长为 \(m\) 的子串,暴力判断是否相同,时间复杂度 \(O(nm)\)。
暴力复杂度不太能接受。
也许在学习了哈希后你会想,上述过程是否可以用哈希优化呢?
但哈希在代码实现上比 KMP 更为复杂,以及常数较大、正确性上的问题也很难使得我们可以“舒服得”使用哈希代替 KMP。
而且 KMP 的核心思想与部分字符串问题的核心思想类似,即在失配的
时候,运用已经配对成功的前缀这个信息。
在匹配过程中,我们一般用两个双指针 \(i,j\),分别指向 \(s\) 串和 \(t\) 串。
假设我们在 \(s[i]\) 和 \(t[j]\) 处发生失配,则:
- 若 \(j==0\),则 \(i++\)。
- 若 \(j>0\),则 \(j=f[j - 1]\)。
若配对成功,我们可以当作 \(s[i]\) 和 \(t[m]\) 发生失配,\(j=f[m-1]\)(\(m\) 为 \(t\) 的长度)。
上述过程中,每次配对成功 \(i,j\) 都会加 \(1\)(最多 \(n\) 次)。
失配时,若 \(j==0\),则 \(i\) 会加 \(1\)(最多 \(n\) 次)。
若 \(j>0\),则 \(j\) 至少减少 \(1\),显然也不超过增加时的 \(n\) 次。也就是 \(s\) 和 \(t\) 串匹配的过程,时间复杂度为 \(O(n)\) 的。
求 \(fail\) 数组的部分与匹配的部分类似。
我们发现,前缀 \(t_0,t_1,\cdots,t_i\) 的 border 去掉末尾的字符后,一定是前缀 \(t_0,t_1,\cdots,t_{i-1}\) 的公共前后缀。
因此,我们考虑从前往后递推计算 \(fail\) 数组。
设当前需要计算 \(f[i]\),我们按长度从大到小遍历前缀 \(t_0,t_1,\cdots,t_{i-1}\) 的所有公共前后缀。
若均未找到或找到某个公共前后缀,使得其后面加一个字符 \(t_i\) 后能成为 \(t_0,t_1,\cdots,t_i\) 的公共前后缀,即为所求 border。