KMP—作用

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时,就会出现重叠的情况。

KMP—作用_第1张图片

如图,红色与绿色部分相同,即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数组的传递性,需要细细地体会体会。

KMP—作用_第2张图片

 

代码

#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;
}

 

你可能感兴趣的:(KMP)