KMP这个入门级的字符串匹配有什么样的功能呢?接下来让我们以caioj1457~1460为例,来进一步挖掘kmp的神奇之处。
首先,要对kmp的p数组(或称为next数组)有深入的理解,它可是kmp的灵魂之处。p[i]指的是由原字符串中前i个字符组成的子串,它的前缀与后缀的最大相同数。换一句话说,对于从头开始的i个字符,前p[i]个字符与后p[i]个字符是完全相同的。注意,这两个子串允许有重叠的地方。
这句话对p数组的解释也十分精辟:p数组是保存某段子串(其实是原串的前缀)的相同前后缀的最大长度,不包括自身。
其实p[i]的是在长为i的前缀中定义的,所以,他不仅适用于整个串,也能适用于它的前缀。如果两串重叠的话,重叠部分必然相同,再有前缀和后缀相同,为子串更丰富的关系提供了第一步条件。
这就算重新讲解了一遍p数组了。下面开始就这个定义剖析kmp的功能。
求某个模式串在文本串中出现的情况
例题:(caioj 1460)
题目描述
给出两个字符串sa和sb,求出sa(模式串)能在sb(文本串)中匹配的最大次数。
思路
这题可以算是模板题了,利用的是p指向的是 与目前后缀相同的 最大的 位置。这样使得模式串回退的位置尽量的少,不至于重头开始。当匹配的位置达到模式串的长度时,就说明sa又出现了一次,此时可以求到模式串与文本串匹配的位置。
代码
#include
#include
#include
using namespace std;
const int maxl=1000010;
int l1,l2;
char s1[maxl],s2[maxl];
int p[maxl];
int main()
{
int T;scanf("%d",&T);
while(T--)
{
scanf("%s%s",s1+1,s2+1);
l1=strlen(s1+1);l2=strlen(s2+1);
p[1]=0;
for(int i=2,j=0;i<=l1;i++)
{
while(s1[j+1]!=s1[i]&&j!=0) j=p[j];
if(s1[j+1]==s1[i]) j++;
p[i]=j;
}
int ans=0;
for(int i=1,j=0;i<=l2;i++)
{
while(s1[j+1]!=s2[i]&&j!=0) j=p[j];
if(s1[j+1]==s2[i]) j++;
if(j==l1) ans++;
}
printf("%d\n",ans);
}
return 0;
}
求有无重复的子串及重复子串的长度和重复次数
例题:(caioj 1457)
题目描述
我们定义两个字符串a和b的乘法: a*b ,就是把它们连接起来。比如: a = "abc" ,b = "def" ,那么 a*b = "abcdef"。
由此推广,字符串的幂运算: a^0 = "" (空字符串) a^(n+1) = a*(a^n)。
给一个字符串s,假设存在 a^n=s,求n的最大值。
思路
可以知道,p[len]小于len/2时,必然没有方法让串由某个子串重复而得。当p[len]>len/2时,就会出现重叠的情况。
如图,红色与绿色部分相同,即p[4]=3。那么显然每一个框是重复的单位,由4个重复单位。由红绿相同可得1和2相同,3和4相同,2和4相同;可知2-3是两串共有的,把2-3看作红色的末尾,则2-3等于3-4;把2-3看作绿色的开头,则2-3等于1-2。故有1-2、2-3和3-4相同,即1与2与3相同,2与3与4相同,可推出1、2、3、4是相同的。
所以当p[len]大于等于len/2时,最多次的重复子串长度为len-p[len],重复次数为len/(len-p[len])。注意到重复次数为整数,则要求len%(len-p[len])等于0;否则不存在重复子串可以构成整个串。
代码
#include
#include
#include
using namespace std;
const int maxn=1000010;
int p[maxn];
char s[maxn];int l;
int main()
{
while(scanf("%s",s+1),s[1]!='.')
{
l=strlen(s+1);
p[1]=0;
for(int i=2,j=0;i<=l;i++)
{
while(s[j+1]!=s[i]&&j>0) j=p[j];
if(s[j+1]==s[i]) j++;
p[i]=j;
}
if(l%(l-p[l])==0) printf("%d\n",l/(l-p[l]));
else printf("1\n");
}
return 0;
}
前缀与后缀是kmp讨论的主要问题,这个作用有些复杂,但是p数组定义的展现。这个作用搞清楚了,kmp的p数组也就彻底明白了
例题:(caioj 1059)
题目描述
给出一个字符串S。找出所有S的前缀等于后缀的情况。按长度递增输出长度。相互之间用空格隔开。
思路
一个答案是S本身。
又一个答案是p[len](p[len]的定义就是从1开始的子串和以len结尾的子串相同的最大长度)。
还有吗?我们很容易想到给p[len]做一个迭代,即长p[p[len]]的前缀仍能满足题目要求。
是真的吗?考虑长p[len]的前缀与长p[p[len]]的前缀,它们的关系其实同长len的前缀与长p[len]的前缀是一样的,也有这样一个关系。事实上,长len的前缀、长p[len]的前缀、长p[p[len]]的前缀有着相同的前缀和后缀,这是p数组的传递性,需要细细地体会体会。
代码
#include
#include
#include
using namespace std;
const int maxl=400010;
char s[maxl];int l;
int p[maxl];
int ans[maxl];
int main()
{
while(scanf("%s",s+1)!=EOF)
{
l=strlen(s+1);
p[1]=0;
for(int i=2,j=0;i<=l;i++)
{
while(s[j+1]!=s[i]&&j!=0) j=p[j];
if(s[j+1]==s[i]) j++;
p[i]=j;
}
int cnt=0;
for(int i=l;i>0;i=p[i])
{
ans[++cnt]=i;
}
for(int i=cnt;i>=1;i--) printf("%d ",ans[i]);
printf("\n");
}
return 0;
}
这个范围就很广了,包括今天提到的所有情况。这应该是解决复杂的前缀问题的一种思路
例题:(caioj 1458)
题目描述
给一个字符串,如果在前 i 位置处满足连续循环A^K(A:单位循环段,K:循环个数),则输出i和K(仅输出K>1的情况,按i的递增顺序)。
思路
这是在子串内研究作用二,对本题的基础思路就不再细讲了。
在处理所有的前缀问题时,一般枚举一遍所有前缀,每次对前缀的问题进行解答,利用好p[1~i]的值。这是利用了p数组的 对原串所有前缀均适用的性质。
代码
#include
#include
#include
using namespace std;
const int maxn=1000010;
int p[maxn];
char s[maxn];int l;
int main()
{
scanf("%s",s+1);
l=strlen(s+1);
p[1]=0;
for(int i=2,j=0;i<=l;i++)
{
while(s[j+1]!=s[i]&&j>0) j=p[j];
if(s[j+1]==s[i]) j++;
p[i]=j;
}
for(int i=1;i<=l;i++)
{
if(i%(i-p[i])==0&&i/(i-p[i])>1) printf("%d %d\n",i,i/(i-p[i]));
}
return 0;
}