原理
失配的时候直接移动模式串到失配的位置(即已经匹配的长度)来加快速度,但是可能会漏掉已经匹配的这部分里前后缀相同的情况,所以需要计算部分匹配值pm
PM=maxlen(前缀=后缀) 前缀不包括最后一个字符 后缀不包括第一个字符
由上可知如果当前位置失配 移动的位数为已经匹配的位数-当前这一位的pm
设当前位置为j 则位数即move=(j-1)-pm[j-1]
令pm右移一位 得到next数组 定义next[0]=-1 因为不可能用到
next[j]=pm[j-1] move=j-1-next[j] 移动之后j的位置即j=j-move=next[j]+1
此时令next=next+1 则当在j位置失配时 j=next[j]
此时next数组的含义恰好为已匹配的字符串的部分匹配值
next数组的计算
以上是next数组的定义推导和手工计算方法 代码采用另外一种更快的方法
将待求的字符串(即模式串)错开一位进行自身的匹配 令上面的指针为i=0下面的指针为j=-1 则它们匹配的长度正好是最大的前后缀的长度 甚至刚好符合前后缀分别不包括最后一个和第一个的定义 用下面一个串的指针j表示正好就是next值
在发生失配的时候 由于i是从0开始的 所以next[j]一定是已经计算过的 可以直接用j=next[j] 代码如下
void getNext(char *str)
{
int i=0,j=-1;
int n=strlen(str);
nxt[0]=-1;
while(i<n)
{
if(j==-1||str[i]==str[j])
{
i++;j++;
nxt[i]=j;
}
else
j=nxt[j];
}
}
练习
hdu1711
题意
下面一个数组在上面的数组第一次出现的位置
int kmp(int *s,int *t,int slen,int tlen)
{
int i=0,j=0;
while(i<slen&&j<tlen)
{
if(j==-1||s[i]==t[j])
{
i++;j++;
}
else
j=nxt[j];
}
if(j==tlen)return i-j+1;
return -1;
}
hdu1686
题意
上面的模式串在下面的子串中可覆盖的出现的次数
注意当找到一个匹配的之后假装在这个地方失配了 达到重复覆盖的计算次数的效果
int kmp_count()
{
int ret=0;
int i=0,j=0;
int slen=strlen(s);
int tlen=strlen(t);
while(i<slen)
{
if(j==-1||s[i]==t[j])
{
i++;j++;
}
else
j=nxt[j];
if(j==tlen)
{
ret++;
j=nxt[j];
}
}
return ret;
}
hdu2087
题意
上面的模式串在下面的子串中不可覆盖的出现的次数
注意当找到一个匹配的之后的策略和上面就不一样了 就直接从头开始匹配
int kmp_count()
{
int ret=0;
int i=0,j=0;
int slen=strlen(s);
int tlen=strlen(t);
while(i<slen)
{
if(j==-1||s[i]==t[j])
{
i++;j++;
}
else
j=nxt[j];
if(j==tlen)
{
ret++;
j=0;
}
}
return ret;
}
hdu3746 循环节问题
题意
一个字符串 问最少在左边或右边加多少字符使得字符串由大于一个循环节组成
题解
- 为什么可以只在右边加?因为题目提示里的环链和循环节的性质都告诉了我们左边和右边是一样的
- 这题和kmp有什么关系?首先我们要认识到 一个符合题意的字符串会有怎样的性质 至少第一个循环节和最后一个循环节是相等的 这其实就是说一个循环节长度的前缀等于后缀 再多想一下 这个字符串是由若干个循环节组成的 那么也就是说除了最后一个循环节的前缀 和 除了第一个循环节的后缀 他们俩是最大的部分匹配值 这就是next数组的定义
- 考虑极端情况 当最后一个位置的后一位next值为len-1的时候 即两个相同的串 上面不动 下面往右移了一位 此时中间对着的len-1位都是匹配的 此时有下面的第1个字符等于上面的第二个字符 下面的第二个字符等于上面的第三个字符… 而上面和下面其实都是一样的 所以就是说整个字符串全是一样的字符。考虑一般情况 由next数组的定义 对齐的长度为next[len](字符串坐标从0开始计数)
int len=n;
int lenB=nxt[len];
int lenA=len-lenB;
int x=lenB%lenA;
if(x==0&&lenB!=0)
ans=0;
else
ans=lenA-x;
printf("%d\n",ans);
hdu1358 循环节问题
题意
输出一个字符串所有可循环的前缀的循环节长度和出现次数
题解
注意一点 虽然next数组是从0开始的 所以对于每一个位置i而言它的部分匹配值应该是nxt[i+1] 但是next数组的含义是已匹配的字符串的部分匹配值 所以对于下面的代码中的i实际就代表了当前处理的前缀长度 而不能考虑从0开始所以要+1
void slove(char* s,int n)
{
int len,tim;//循环节的长度和循环节出现的次数
for(int i=1;i<=n;i++)//tim>1所以n其实没有影响
{
len=i-nxt[i];
tim=i/len;
if(len!=i&&tim>1&&len*tim==i)
printf("%d %d\n",i,tim);
}
}
hust1010 循环节问题
题意
有一个字符串A,假设是”abcdefg”,由A可以重复组成AAA,从中截取一部分(至少包含一个以上完整A)为B。给出字符串B,求A最短的长度。
题解
第一个循环节问题的变形
void slove(int n)
{
int len=n-nxt[n];//此处似乎存疑
printf("%d\n",len);
}
poj2406 循环节问题
题意
循环节出现的次数
题解
len=n-1-nxt[n-1];
ans=n/len;
if(ans*len!=n)ans=1;//这一行不能漏
printf("%d\n",ans);
poj2752 前缀等于后缀的所有值
题意
rt
题解
next数组的理解 已匹配的字符串的部分匹配值 所以从最后一个位置开始的每个next值都是匹配值 另外注意next数组的计算是先更新i、j再更新next值
void slove(char *s,int n)
{
vector<int>ans;
int t=n;
while(nxt[t])
{
ans.push_back(nxt[t]);
t=nxt[t];
}
int l=ans.size();
for(int i=l-1;i>=0;i--)
{
printf("%d ",ans[i]);
}
printf("%d\n",n);
}
poj3080 简单应用
题意
10个长度为60的字符串的最长公共子串
题解
由于题目数据范围的限制 所以直接枚举就可以了 码力下降的太厉害了 代码量上一点就不敢动了 对于不确定的细节不要想着莽4
ans="";
for(int i=0;i<60;i++)
{
int now=max(3,(int)ans.size());
for(int j=now;i+j<=60;j++)
{
string tt=s[0].substr(i,j);
getNext(tt,j);
bool flag=false;
for(int k=0;k<n;k++)
{
if(!kmp(s[k],tt))
{
flag=true;
break;
}
}
if(!flag)
{
if((int)ans.size()<j)
ans=tt;
else if((int)ans.size()==j)
ans=ans<tt?ans:tt;
}
}
}
if(ans=="")
ans="no significant commonalities";