CF1721E 题解(KMP学习笔记)
CF1721E 题解
题面
前置知识
KMP,基本的字符串函数。
(不懂 KMP 的可以翻到最底下看看后记)
思路
对于每个询问,其实只要把 t 接在 s 的后面跑 KMP 即可。(不懂 KMP 的看后记!)
但这样会 TLE。。。
很遗憾,那就多设一个数组 last。
设 lasti,j 表示第 i 个前缀的所有 border(最长公共前后缀)中,最长的使得下一位刚刚好是 j 的位置。
如:若 si+1=j,则 lasti,j=i,否则 lasti,j=lastfi,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 的前缀 t0,t1,⋯,ti 的 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 数组的部分与匹配的部分类似。
我们发现,前缀 t0,t1,⋯,ti 的 border 去掉末尾的字符后,一定是前缀 t0,t1,⋯,ti−1 的公共前后缀。
因此,我们考虑从前往后递推计算 fail 数组。
设当前需要计算 f[i],我们按长度从大到小遍历前缀 t0,t1,⋯,ti−1 的所有公共前后缀。
若均未找到或找到某个公共前后缀,使得其后面加一个字符 ti 后能成为 t0,t1,⋯,ti 的公共前后缀,即为所求 border。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现