字符串(KMP系列)

一. KMP

1. border

如果一个字符串的前缀等于他的后缀,我们称这个字符串是原字符串的border。
例如,abcakmeabc 中,abc 就是一个border。

2. KMP

定义一个字符串的 nxtnxt 数组表示:前 ii 个字符的最长border长度。
那怎么求一个字符的nxt呢?代码如下:

void kmp(int n,string b)
{
      int j=0;
      for(int i=2;i<=n;i++)
      {
          while(j&&b[i]!=b[j+1]) j=nxt[j];
          if(b[j+1]==b[i]) j++;
          nxt[i]=j;
      }
}

时间复杂度 O(n)O(n)
KMP经常用来求解模式串匹配的问题。即询问 sstt 中出现的次数。一般来说,代码如下:

j=0;
for(int i=1;i<=m;i++)
{
		while(j>0&&b[j+1]!=a[i])
			j=nxt[j];
		if(b[j+1]==a[i]) 
			j++;
		if(j==n)
		{
			cout<<i-n+1<<endl;
			j=nxt[j];
		}
}

其中,m=t,n=sm=|t|,n=|s|

原因自己上题解搜。

二. exKMP/扩展KMP/Z函数

1. 什么是 Z 函数

注:字符串从 00 开始编号。

什么是 Z\text Z 函数呢?我们定义 ziz_i 表示 ssss 的每一个后缀 in1i\sim n-1 的最长公共前缀(LCP)的长度,此时我们称 zzssZ\text Z 函数。特殊的,z[0]=0\mathbf{z[0]=0}

举几个栗子:

z(z(aaaaa)=[0,4,3,2,1])=[0,4,3,2,1]z(z(aaabaab)=[0,2,1,0,2,1,0])=[0,2,1,0,2,1,0]

2. Z 函数线性求法

就像 nxtnxt 一样,Z 函数也可以在 O(n)O(n) 复杂度中求。

首先假设已知 z0,z1,,zi1z_0,z_1,\cdots,z_{i-1},现在要求 ziz_i

如果 ziz_i 被求出来了,那么 s[i,i+zi1]s[i,i+z_i-1] 一定是 ss 的前缀,且 s[i,i+zi]s[i,i+z_i] 一定ss 的前缀。那么现在正在处理 ziz_i,区间 s[i,i+zi1]s[i,i+z_i-1] 就称为我们的匹配段,简称 Z-box。

由于 LaTeX\LaTeX 太难敲了,所以我们简称 [l,r][l,r] 是我们的匹配段。由于左端点已知,我们只需要扩展右端点。根据定义,[l,r][l,r]ss 的前缀。我们维护的时候 lil\le i,初始 l=r=0l=r=0

分两种情况讨论

  • 当前处理的 iirr 的左边(iri\le r):由于是匹配段,那么同一长度的 ss 肯定是相等的!(因为他们都是 ss 的前缀,长度又相等,必然相同)就有性质:s[i,r]=s[il,rl]s[i,r]=s[i-l,r-l]。所以 zi=zilz_i=z_{i-l}?别忘了,z[i]z[i] 的长度一定不超过 ri+1r-i+1(隔着超过字符串长度了?)所以 zimin{zil,ri+1}z_i\ge\min\{z_{i-l},r-i+1\}
    • 如果 zil<ri+1z_{i-l}<r-i+1,那么 zi=zilz_i=z_{i-l}
    • 如果 zi1ri+1z_{i-1}\ge r-i+1,那么 zi=ri+1z_i=r-i+1 的基础上,暴力扩展 ziz_i
  • 如果 i>ri>r,暴力扩展

最后,如果 i+zi1>ri+z_i-1>r,则令 l=i,r=i+zi1l=i,r=i+z_i-1 即可。

3. 代码

for(int i=1;i<n;i++)
{
	int len=r-i+1;
	if(i<=r&&z[i-l]<len) z[i]=z[i-l];
	else
	{
		z[i]=max(0ll,len);
		while(i+z[i]<n&&a[z[i]]==b[i+z[i]]) z[i]++;
	}
	if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}

4. 时空复杂度分析

容易发现,内层 rr 每次至少加一,外层 ii 每层加一。总的时间复杂度 O(n)O(n)

posted @ 2024-02-05 15:12  sLMxf  阅读(50)  评论(0)    收藏  举报  来源