KMP学习笔记
首先讲讲 KMP 是怎么实现的。
题目:
题目:
求模式串在待匹配串中的出现次数。
明显可以直接暴力枚举,首先确定左端点,然后向右跑即可
考虑优化这一过程
我们考虑在整个字符串中匹配以某个字符为末尾的最长匹配
那么我们直接跑过去,如果完全匹配,最优情况下肯定是 \(O(n)\) 的
但是如果不是最优情况呢
那么我们肯定要先去找到模式串中可以与这一位匹配的
但还有一个要求就是必须要前面完全匹配上了才行
怎么搞?
我们考虑到假设我们目前匹配了 \(1\sim r\) 的模式串,然而在 \(r+1\) 位置没有匹配上
为了匹配上,我们不得不重新匹配 \(1\sim l\) 并让 \(l+1\) 进行匹配,当然有要求,就是 \(1\sim l\) 应该与 \(r-l+1\sim r\) 相同
那么我们就是要找出以 \(r\) 为末尾的模式串的最长相等的前缀和后缀
这就是 KMP 算法的核心了
考虑如何处理出这一东西,处理出来后,由于长度每次都必定减少,然而每次都只+1,所以是 \(O(n)\) 的
如何处理?
类似处理为一个字符串与自己匹配,前缀为模式串,后缀为匹配的字符串
那么我们就能借助前面求出来的结果推出后面的结果了
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int T,f[1000005],lent,lens;
char s[1000005],t[1000005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=lent;i++){
while(~j&&s[i]!=s[j+1])j=f[j];
f[i]=++j;
}
}
int kmp(){
int res=0,j=0;
for(int i=1;i<=lens;i++){
while(~j&&s[i]!=t[j+1])j=f[j];
j++;
if(j==lent)res++;
}
return res;
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%s%s",t+1,s+1);
lens=strlen(s+1),lent=strlen(t+1);
init();
printf("%d\n",kmp());
}
return 0;
}
感觉后面的都比较板子了,挑几道稍微有点东西的说说
人随着岁数的增长是越大越聪明还是越大越笨,这是一个值得全世界科学家思考的问题,同样的问题Eddy也一直在思考,因为他在很小的时候就知道亲和串如何判断了,但是发现,现在长大了却不知道怎么去判断亲和串了,于是他只好又再一次来请教聪明且乐于助人的你来解决这个问题。
亲和串的定义是这样的:给定两个字符串s1和s2,如果能通过s1循环移位,使s2包含在s1中,那么我们就说s2 是s1的亲和串。Input
本题有多组测试数据,每组数据的第一行包含输入字符串s1,第二行包含输入字符串s2,s1与s2的长度均小于100000。
Output
如果s2是s1的亲和串,则输出"yes",反之,输出"no"。每组测试的输出占一行。
直接首尾相连搞一遍就ok了
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char s1[100005],s2[100005];
int len1,len2,f[100005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len2;i++){
while(~j&&s2[i]!=s2[j+1])j=f[j];
f[i]=++j;
}
}
int kmp(){
int j=0;
for(int i=1;i<=len1;i++){
while(~j&&s1[i]!=s2[j+1])j=f[j];
j++;
if(j==len2)return true;
}
for(int i=1;i<=len1;i++){
while(~j&&s1[i]!=s2[j+1])j=f[j];
j++;
if(j==len2)return true;
}
return false;
}
signed main(){
while(~scanf("%s%s",s1+1,s2+1)){
len1=strlen(s1+1);
len2=strlen(s2+1);
if(len1<len2){
puts("no");
continue;
}
init();
if(kmp())puts("yes");
else puts("no");
}
return 0;
}
给定一个字符串,一次操作可以在这个字符串的右边增加任意一个字符。求操作之后的最短字符串,满足操作结束后的字符串是回文。
比较有意思,由于回文翻转后不变,我们拿翻转后的作为模式串来匹配,原字符串,就可以得到右端不变时的最长回文,这样左边不在回文的就可以计算出来了
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char s1[1000005],s2[1000005];
int T,len,f[1000005],cas;
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len;i++){
while(~j&&s1[i]!=s1[j+1])j=f[j];
f[i]=++j;
}
}
int kmp(){
int j=0;
for(int i=1;i<=len;i++){
while(~j&&s2[i]!=s1[j+1])j=f[j];
j++;
}
return j;
}
signed main(){
int hhq;
scanf("%d",&T);
while(T--){
scanf("%s",s2+1);
len=strlen(s2+1);
for(int i=1;i<=len;i++)s1[i]=s2[len-i+1];
init();
printf("Case %d: %d\n",++cas,2*len-kmp());
}
return 0;
}
是时候来点音乐了!我们邀请了许多流行音乐家参加音乐节。他们每个人都会演奏自己代表性的歌曲。为了使节目更有趣和具有挑战性,主持人们将在歌曲的节奏中添加一些限制,即每首歌都需要有一个“主题部分”。主题部分应在每首歌的开头、中间和结尾播放。更具体地说,给定主题部分 E,歌曲的格式将为“EAEBE”,其中部分 A 和部分 B 可以有任意数量的音符。请注意,有 26 种音符,用小写字母“a”-“z”表示。
为了为音乐节做好充分准备,主持人想知道每首歌的主题部分可能的最大长度。你能帮帮我们吗?
输入格式
第一行一个整数 N,表示音乐节上的歌曲总数。接下来 N 行,每行一个字符串,表示第 i 首歌曲的音符。字符串的长度不超过 106106。
输出格式
共 N 行,每行一个整数,表示第 i 首歌曲的主题部分可能的最大长度。
既然有相同部分,我们考虑用KMP来匹配
那么我们就枚举中间的与前缀的匹配,后面的与前缀匹配,枚举出来
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
using namespace std;
int T,len,f[1000005];
char s[1000005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len;i++){
while(~j&&s[j+1]!=s[i])j=f[j];
f[i]=++j;
}
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%s",s+1);
len=strlen(s+1);
init();
int res=0;
for(int i=1;i<=len;i++)res=max(res,min(f[i],min(f[len],len-i+1)));
printf("%d\n",res);
}
return 0;
}
对于给定的字符串 n�,标记从 11 到 n�,你应该找到最大的 i (1≤i≤n)� (1≤�≤�),使得存在一个整数 j (1≤j<i)� (1≤�<�),并且 Sj�� 不是 Si�� 的子串。
一个字符串 Si�� 的子串是另一个字符串 Si�� 中出现的字符串。例如,"ruiz" 是 "ruizhang" 的子串,而 "rzhang" 不是 "ruizhang" 的子串。
输入
第一行包含一个整数 t (1≤t≤50)� (1≤�≤50),表示测试用例的数量。
对于每个测试用例,第一行是正整数 n (1≤n≤500)� (1≤�≤500),接下来的 n� 行列出了字符串 S1,S2,⋯,Sn�1,�2,⋯,��。
所有字符串都由小写字母给出,且字符串长度不超过 20002000 个字母。输出
对于每个测试用例,输出你得到的最大标签。如果不存在,则输出 −1−1。
注意到是存在一个,并且要求不是子串
简单的想法,如果一个字符串a是b的子串,那么如果a不是c的子串,b也一定不是c的子串
那么我们就可以来个双指针,如果发现其是当前子串,就指针右移,即可得到答案
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
using namespace std;
int T,n,cas,f[2005],lenx,leny;
char s[505][2005];
void init(int x){
f[0]=-1;
for(int i=1,j=-1;i<=lenx;i++){
while(~j&&s[x][i]!=s[x][j+1])j=f[j];
f[i]=++j;
}
}
bool zc(int x,int y){
lenx=strlen(s[x]+1),leny=strlen(s[y]+1);
init(x);
for(int i=1,j=0;i<=leny;i++){
while(~j&&s[x][j+1]!=s[y][i])j=f[j];
j++;
if(j==lenx)return true;
}
return false;
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%s",s[i]+1);
int w=1,res=-1;
for(int i=2;i<=n;i++){
while(w<i&&zc(w,i))w++;
if(w<i)res=i;
}
printf("Case #%d: %d\n",++cas,res);
}
return 0;
}
给定一个字符串s,求s的每个前缀在此字符串中出现的次数,然后对次数求和,然后再对10007取模,就是要输出的答案。
由于可能一个前缀和自己相同的后缀匹配上,因此我们从后往前进行 dp,将自己的 dp 值给传到前面最近的公共前后缀
直接处理每个位置的前后缀,向前传递即可
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
using namespace std;
const int mod=10007;
int T,n,f[200005],dp[200005];
char s[200005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=n;i++){
while(~j&&s[j+1]!=s[i])j=f[j];
f[i]=++j;
}
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
scanf("%s",s+1);
init();
for(int i=1;i<=n;i++)dp[i]=0;
int ans=0;
for(int i=n;i>=1;i--){
dp[i]=(dp[i]+1)%mod;
dp[f[i]]=(dp[f[i]]+dp[i])%mod;
ans=(ans+dp[i])%mod;
}
printf("%d\n",ans);
}
return 0;
}
今天是SF的生日,所以VS给了SF两个字符串S1和S2作为礼物,其中包含一个很大的秘密。SF对这个秘密很感兴趣,于是问VS如何获取它。以下是VS告诉他的事情:
后缀(S2,i) = S2[i...len]。Ni是后缀(S2,i)在S1中出现的次数,Li是后缀(S2,i)的长度。那么秘密就是Ni和Li的乘积之和。
现在SF希望你帮他找到这个秘密。答案可能非常大,所以答案应该对1000000007取模。
可以理解为上一题的魔改版,由于后缀不方便处理,先reverse一遍即可
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
#define int long long
using namespace std;
const int mod=1e9+7;
int T,len1,len2,f[1000005],dp[1000005];
char s1[1000005],s2[1000005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len2;i++){
while(~j&&s2[j+1]!=s2[i])j=f[j];
f[i]=++j;
}
}
void kmp(){
for(int i=1,j=0;i<=len1;i++){
while(~j&&s1[i]!=s2[j+1])j=f[j];
j++;
dp[j]++;
}
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%s%s",s1+1,s2+1);
len1=strlen(s1+1),len2=strlen(s2+1);
reverse(s1+1,s1+len1+1),reverse(s2+1,s2+len2+1);
for(int i=1;i<=len2;i++)dp[i]=0;
init();
kmp();
int ans=0;
for(int i=len2;i>=1;i--){
dp[f[i]]+=dp[i];
ans+=i*dp[i];
ans%=mod;
}
printf("%d\n",ans);
}
return 0;
}
给出字符串的长度n,以及该字符串是由哪些小写字母组成,现给出一个坏串S,求存在多少种不同的字符串,使得其子串不含坏串。
原题面的 n 比较大,所以考虑矩阵优化,记录下从第 \(i\) 匹配到第 \(j\) 个的方案数,这通过 KMP 得到即可,然后矩阵快速幂
代码:
#include<bits/stdc++.h>
using namespace std;
int n,T,len1,len2,f[55],pos[305],cas;
char s1[55],s2[55];
struct MT{
unsigned c[55][55];
MT(){
memset(c,0,sizeof(c));
}
void reset(){
memset(c,0,sizeof(c));
}
MT friend operator*(MT a,MT b){
MT c;
for(int i=0;i<len2;i++){
for(int j=0;j<len2;j++){
for(int k=0;k<len2;k++)c.c[i][j]+=a.c[i][k]*b.c[k][j];
}
}
return c;
}
}base;
int get(int x,int y){
int j=x;
while(~j&&pos[s2[j+1]]!=y)j=f[j];
return ++j;
}
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len2;i++){
while(~j&&s2[i]!=s2[j+1])j=f[j];
f[i]=++j;
}
}
MT calc(int n){
MT s;
s.c[0][0]=1;
while(n){
if(n&1)s=s*base;
base=base*base;
n>>=1;
}
return s;
}
signed main(){
int hhq;
scanf("%d",&T);
while(T--){
scanf("%d",&n);
scanf("%s%s",s1+1,s2+1);
len1=strlen(s1+1),len2=strlen(s2+1);
memset(pos,0,sizeof(pos));
base.reset();
for(int i=1;i<=len1;i++)pos[s1[i]]=i;
init();
for(int i=0;i<len2;i++){
for(int j=1;j<=len1;j++){
int x=get(i,j);
if(x==len2)continue;
base.c[i][x]++;
}
}
MT res=calc(n);
unsigned ans=0;
for(int i=0;i<len2;i++)ans+=res.c[0][i];
printf("Case %d: %u\n",++cas,ans);
}
return 0;
}
cgg给你一个字符串s,问在所有的[0, i]区间是否有完整的循环节,若有,输出i并输出循环次数。(嗯哼,就是这么简单的题意)
应该是比较板子了,就一个性质,可以用KMP求最长循环节,证明方法类似哈希求循环节
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
using namespace std;
int n,f[1000005],cas;
char s[1000005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=n;i++){
while(~j&&s[j+1]!=s[i])j=f[j];
f[i]=++j;
}
}
signed main(){
while(~scanf("%d",&n)){
if(!n)break;
scanf("%s",s+1);
init();
printf("Test case #%d\n",++cas);
for(int i=1;i<=n;i++){
int len=i-f[i];
if(i%len==0&&i/len>=2)printf("%d %d\n",i,i/len);
}
}
return 0;
}
假设s可以由t重复k次拼成,即s=tttt……tt,我们称为s=tk,k是s的幂次。先给定一个字符串s,求最大的幂次n使得存在t满足s=tn。
(什么怪异题面懒得管了)
要求最长周期,那么就需要求最小循环节
通过性质可知
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
using namespace std;
char s[1000005];
int len,f[1000005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len;i++){
while(~j&&s[j+1]!=s[i])j=f[j];
f[i]=++j;
}
}
signed main(){
while(~scanf("%s",s+1)){
len=strlen(s+1);
if(len==1&&s[1]=='.')break;
init();
int l=len-f[len];
if(!l||len%l)puts("1");
else printf("%d\n",len/l);
}
return 0;
}
Every morning when they are milked, the Farmer John's cows form a rectangular grid that is R (1 <= R <= 10,000) rows by C (1 <= C <= 75) columns. As we all know, Farmer John is quite the expert on cow behavior, and is currently writing a book about feeding behavior in cows. He notices that if each cow is labeled with an uppercase letter indicating its breed, the two-dimensional pattern formed by his cows during milking sometimes seems to be made from smaller repeating rectangular patterns.
Help FJ find the rectangular unit of smallest area that can be repetitively tiled to make up the entire milking grid. Note that the dimensions of the small rectangular unit do not necessarily need to divide evenly the dimensions of the entire milking grid, as indicated in the sample input below.
Input
* Line 1: Two space-separated integers: R and C
* Lines 2..R+1: The grid that the cows form, with an uppercase letter denoting each cow's breed. Each of the R input lines has C characters with no space or other intervening character.
为什么 cpp 放的是英文题面啊
不管了,反正就是先哈希,再匹配哈希值
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
#define int long long
using namespace std;
int T,len,f[100005];
char s[100005];
void init(){
f[0]=-1;
for(int i=1,j=-1;i<=len;i++){
while(~j&&s[i]!=s[j+1])j=f[j];
f[i]=++j;
}
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%s",s+1);
len=strlen(s+1);
init();
int x=len-f[len];
if(x==len)printf("%d\n",len);
else if(len%x==0)puts("0");
else printf("%d\n",x-len%x);
}
return 0;
}
对于一个仅含小写字母的字符串 , 为 的前缀且 ,那么我们称 为 的 proper 前缀。
规定字符串 表示 的周期,当且仅当 是 的 proper 前缀且 是 的前缀。若这样的字符串不存在,则 的周期为空串。
例如
ab是abab的一个周期,因为ab是abab的 proper 前缀,且abab是ab+ab的前缀。求给定字符串所有前缀的最大周期长度之和。
打比赛的时候不会水,最后两分钟终于写出来了
假设一个字符串为 ABCDEFGH
假设最长的相同的前后缀长度为 7
那么可以得到 A=B=C=D=E=F=G=H
同理在长度为 5 时,则 ABC=DEF,DE=GH,即A=D=G,B=E=H,C=F
这就和那个通过哈希求循环节道理一样
那么我们就可以通过一直向前跳一位,跳过一个循环节,看看最后的循环节能不能分成更小的循环节
可以直接路径压缩解决
代码:
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
const int base=2333;
int k,f[1000005];
char s[1000005];
ull h[1000005],pw[1000005];
ull gethash(int l,int r){
return h[r]-h[l-1]*pw[r-l+1];
}
int find(int x){
if(!f[x])return x;
return f[x]=find(f[x]);
}
signed main(){
cin>>k;
cin>>s+1;
f[0]=-1;
for(int i=1,j=-1;i<=k;i++){
while(~j&&s[j+1]!=s[i])j=f[j];
f[i]=++j;
}
int ans=0;
for(int i=1,j=0;i<=k;i++){
int w=i;
w=find(w);
if(2*w<=i)ans+=i-w;
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号