不是很算详解吧
算法用途
KMP算法,俗称看毛片算法#(滑稽),用于求字符串的某一子串在该字符串中出现的次数(可能也不是其子串),也可以和其他算法结合起来用(比如和Trie树搞搞AC自动机什么的)
算法思想
预处理出一个next数组,实现快速跳转。其中next[i]存的是到第i个字符为止的字符串,其后缀与其前缀相等的最大长度。
感觉好难理解有木有!别急,我们来举个栗子。
比如说下面两个字符串:
s1串:bacacacbcacaca
s2串:cacaca
现在我们在s1串中找s2串。
第一次匹配:s1的b–>s2的c 发现并不一样,怎么办??把B串往后移一个呗!
s1串:bacacacbcacaca
s2串:0cacaca(空格打不出来 (╯﹏╰) 用0代替)
然而还是不一样嘛!好气哦!只能再移一个喽!
s1串:bacacacbcacaca
s2串:00cacaca
s1的c–>s2的c 终于一样啦!接下去几个亦如此!
但是最后一个不一样呢!又要一个一个移?这不是暴力嘛!
不不不,这次我们不一个一个移,而是两个一起移!为什么呢?之前我们提到过next数组(可以返回去看里面存的是什么)。然后我们再来看s2当前的串:cacac,其后缀cac与其前缀cac恰好相等,且此长度是最长的(当然自身不能算啦),移的长度便是:length(cacac)-length(cac)=2。
s1串:bacacac|bcacaca
s2串:00cacac|a
但是为什么这么移是可行的呢?我们看到,因为其后缀cac已经是比较过了,说明到这里为止都是匹配的,再把其相同的前缀拿过来,就省去了之前的一步一步移,以及无用的比较。
那有些人就要问了,如果是像ccccc这样的怎么办?不是会跳过去么?
我们能看到,ccccc的最长相同的前后缀为cccc(除去自己),其长度减去其最长相同前后缀的长度恰好为1,因此并无大碍。
于是移过后继续比较,直至找到目标即可。
算法主要代码:
node=0;
int ans=0;
for (int i=0;i<n;i++){
while(node&&s2[node]!=s1[i])
node=nxt[node-1];
if (s2[node]==s1[i])
node++;
if (node==m){
ans++;
node=nxt[node-1];
}
}
算法关键
整个算法的关键就是next数组。next数组要怎么求呢?
首先,根据它里面存的东西,我们可以判断:它肯定和寻找的那个串有关系,和作为寻找范围的串没有半毛钱关系。
然后,因为要找相同的前后缀,即要找到一个j,使得s[1->j]=s[i-j+1->i],这与之前的比较过程是非常相似的。
于是我们就可以推断出构造next数组的代码(讲的不是很清楚,请读者们自行体会其中的奥秘):
int node=0;
for (int i=1;i<m;i++){
while(node&&s2[node]!=s2[i])
node=nxt[node-1];
if (s2[node]==s2[i])
node++;
nxt[i]=node;
}
模板题:hihocoder1015、洛谷3375(hihocoder上有更详细的解释,还不懂的小伙伴可以去看看)
代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#define MAXN 1000000
using namespace std;
char s1[MAXN+5],s2[MAXN+5];
int t,n,m;
int nxt[MAXN+5];
int main(){
scanf("%d",&t);
while (t--){
scanf("%s%s",s2,s1);
n=strlen(s1); m=strlen(s2);
memset(nxt,0,sizeof(nxt));
int node=0;
for (int i=1;i<m;i++){
while(node&&s2[node]!=s2[i])
node=nxt[node-1];
if (s2[node]==s2[i])
node++;
nxt[i]=node;
}
node=0;
int ans=0;
for (int i=0;i<n;i++){
while(node&&s2[node]!=s1[i])
node=nxt[node-1];
if (s2[node]==s1[i])
node++;
if (node==m){
ans++;
node=nxt[node-1];
}
}
printf("%d\n",ans);
}
return 0;
}
算法拓展
最小循环节
若子串t通过不断重复可以构成s且t的长度最小,则t为s的最小循环节。
举个栗子:子串ad是字符串adadadad的最小循环节。
要怎么求最小循环节呢?
设s存在最小循环节t,那么t的长度即为strlen(s)-next[strlen(s)]。t即为s[next[strlen(s)]~strlen(s)]。(仔细体会下为什么)
不难发现,只要求出s的next数组就可以求出s的最小循环节。所以我们像KMP那样求一遍next数组就可以啦!
代码(并未评测过,有疑问请指出):
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
char s[100];
int nxt[100];
int main(){
scanf("%s",s);
int n=strlen(s);
//求next数组
int node=0;
memset(nxt,0,sizeof(nxt));
for (int i=1;i<n;i++){
while (node&&s[i]!=s[node])
node=nxt[node-1];
if (s[i]==s[node])
node++;
nxt[i]=node;
}
//如果存在循环节
if (nxt[n-1]&&n%(n-nxt[n-1])==0)
printf("Yes\n");
else{
printf("No\n");
return 0;
}
int l=n-nxt[n-1];//最小循环节长度
for (int i=0;i<l;i++)
printf("%c",s[i]);
printf("\n");
return 0;
}