【字符串算法】扩展 KMP 算法(Z-Algorithm)

【字符串算法】扩展 KMP 算法(Z-Algorithm)


算法专栏:扩展 KMP 算法
算法竞赛:字符串,模式匹配,扩展 KMP 算法
洛谷专栏:模板,字符串,扩展 KMP 算法
洛谷模板:P5410 扩展 KMP(Z 函数)
题目链接:洛谷 P5410 扩展 KMP

题目描述:
给定两个字符串 a , b a,b a,b,你要求出两个数组:

  • b b b z z z 函数数组 z z z,即 b b b b b b 的每一个后缀的 LCP 长度。
  • b b b a a a 的每一个后缀的 LCP 长度数组 p p p

对于一个长度为 n n n 的数组 a a a,设其权值为 xor ⁡ i = 1 n i × ( a i + 1 ) \operatorname{xor}_{i=1}^n i \times (a_i + 1) xori=1ni×(ai+1)

输入格式:
两行两个字符串 a , b a,b a,b
输出格式:
第一行一个整数,表示 z z z 的权值。第二行一个整数,表示 p p p 的权值。

数据范围:
对于第一个测试点, ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 3 |a|,|b| \le 2 \times 10^3 a,b2×103
对于第二个测试点, ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 5 |a|,|b| \le 2 \times 10^5 a,b2×105
对于 100 % 100\% 100% 的数据, 1 ≤ ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 7 1 \le |a|,|b| \le 2 \times 10^7 1a,b2×107,所有字符均为小写字母。


扩展 KMP(Z-Algorithm)

扩展 KMP(exKMP)算法,也被称作 Z 函数(Z-Algorithm),顾名思义就是 KMP 算法的扩展算法,是一种字符串模式匹配算法,可以在 O ( n ) \mathcal{O}(n) O(n) 的时间复杂度内求解字符串后缀与前缀的最长公共前缀(LCP)长度问题。


问题描述

给定两个字符串 a , b a,b a,b,你要求出两个数组:

  • b b b z z z 函数数组 z z z,即 b b b b b b 的每一个后缀的 LCP 长度。
  • b b b a a a 的每一个后缀的 LCP 长度数组 p p p

LCP 长度(Longest Common Prefix),指两个字符串或序列的最长公共前缀长度。在此问题中,两个字符串分别为给定串和给定串的其中某个后缀子串。

对于一个长度为 n n n 的字符串 S [ 0 ∼ n − 1 ] S[0\sim n-1] S[0n1](字符串 b b b), z z z 数组定义为:

  • z [ 0 ] = n z[0]=n z[0]=n(整个字符串与自身完全匹配);
  • 对于 i > 0 i>0 i>0 z [ i ] z[i] z[i] 表示从位置 i i i 开始的子串 S [ i ∼ n − 1 ] S[i\sim n-1] S[in1] S S S 的前缀 S [ 0 ∼ n − 1 ] S[0\sim n-1] S[0n1] 的最长公共前缀的长度。

对于一个长度为 m m m 的字符串 S ′ [ 0 ∼ m − 1 ] S'[0\sim m-1] S[0m1](字符串 a a a), p p p 数组定义为:

  • 对于 i ≥ 0 i\ge 0 i0 p [ i ] p[i] p[i] 表示从位置 i i i 开始的子串 S ′ [ i ∼ m − 1 ] S'[i\sim m-1] S[im1] S S S 的前缀 S [ 0 ∼ n − 1 ] S[0\sim n-1] S[0n1] 的最长公共前缀的长度。

对数组 p p p 和数组 z z z 的定义进行观察可以发现,可以通过将字符串 a a a 拼接到字符串 b b b 后求 z z z 数组,间接求解 p p p 数组。


实例展示

字符串 b b b

事物内容内容内容内容内容内容内容
字符串 b b b a a a a a a a a a b b b b b b a a a a a a
数组 z z z 7 7 7 2 2 2 1 1 1 0 0 0 0 0 0 2 2 2 1 1 1

字符串 a a a

事物内容内容内容内容内容内容内容内容内容内容
字符串 a a a a a a a a a a a a a a a a a a b b b b b b b b b a a a a a a
数组 p p p 3 3 3 3 3 3 5 5 5 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 2 2 2 1 1 1

扩展 KMP 算法

算法思想

扩展 KMP 算法是根据前面求解出来的结果递推式地向后扩展求解

扩展 KMP 算法在处理字符串时是从前向后依次遍历地处理每一位字符,对于一个字符串 S [ 0 ∼ n − 1 ] S[0\sim n-1] S[0n1],如果已经知道 z [ 0 ∼ i − 1 ] z[0\sim i-1] z[0i1] 的结果,就能递推式地求解出 z [ i ] z[i] z[i] 的值。


算法思路

对于一个长度为 n n n 的字符串 S [ 0 ∼ n − 1 ] S[0\sim n-1] S[0n1],求解其 z z z 数组,已知 z [ 0 ∼ i − 1 ] z[0\sim i-1] z[0i1] 时,对于 0 ≤ l < i 0\le l < i 0l<i,维护一个 l l l,使 l + z [ l ] − 1 l+z[l]-1 l+z[l]1 最大,记作 r r r,即 r = l + z [ l ] − 1 r=l+z[l]-1 r=l+z[l]1

将字符串 S S S 表示在图片上,如图所示:

根据 z z z 数组的定义,可以知道 s [ 0 ∼ r − l ] = s [ l ∼ r ] s[0\sim r-l]=s[l\sim r] s[0rl]=s[lr],即图中蓝色的两部分是完全相同的,分两种情况进行讨论:

(情况一):

i − l + z [ i − l ] − 1 ≤ r − l i-l+z[i-l]-1\le r-l il+z[il]1rl(即 i + z [ i − l ] − 1 ≤ r i+z[i-l]-1\le r i+z[il]1r)时,由蓝色部分一致性可知 z [ i ] = z [ i − l ] z[i]=z[i-l] z[i]=z[il]

如图所示:

  • P a r t ( 1 ) Part(1) Part(1) 部分即 S [ 0 ∼ z [ i − l ] − 1 ] S[0\sim z[i-l]-1] S[0z[il]1]
  • P a r t ( 2 ) Part(2) Part(2) 部分即 S [ i − l ∼ i − l + z [ i − l ] − 1 ] S[i-l\sim i-l+z[i-l]-1] S[ilil+z[il]1]
  • P a r t ( 3 ) Part(3) Part(3) 部分即 S [ i ∼ i + z [ i − l ] − 1 ] S[i\sim i+z[i-l]-1] S[ii+z[il]1],即待求部分。
  • P a r t ( 4 ) Part(4) Part(4) 部分即 S [ i − l ∼ z [ l ] − 1 ] S[i-l\sim z[l]-1] S[ilz[l]1],即从 i − l i-l il 到第一个蓝色部分最后。
  • P a r t ( 5 ) Part(5) Part(5) 部分即 S [ i ∼ l + z [ l ] − 1 ] S[i\sim l+z[l]-1] S[il+z[l]1],即从 i i i 到第二个蓝色部分最后。

思路过程:

  1. 已知 P a r t ( 5 ) Part(5) Part(5) 部分,即 S [ i ∼ l + z [ l ] − 1 ] S[i\sim l+z[l]-1] S[il+z[l]1],找到与 P a r t ( 5 ) Part(5) Part(5) 对应的 P a r t ( 4 ) Part(4) Part(4) 部分,即 S [ i − l ∼ z [ l ] − 1 ] S[i-l\sim z[l]-1] S[ilz[l]1]
  2. 标记出从 i − l i-l il 开始的后缀的前缀中与前缀的最长公共部分,记作 P a r t ( 2 ) Part(2) Part(2) 部分,即 S [ i − l ∼ i − l + z [ i − l ] − 1 ] S[i-l\sim i-l+z[i-l]-1] S[ilil+z[il]1],对应的前缀部分标记为 P a r t ( 1 ) Part(1) Part(1) 部分,即 S [ 0 ∼ z [ i − l ] − 1 ] S[0\sim z[i-l]-1] S[0z[il]1]
  3. P a r t ( 5 ) Part(5) Part(5) 部分找出与 P a r t ( 2 ) Part(2) Part(2) 对应的部分,即 S [ i ∼ i + z [ i − l ] − 1 ] S[i\sim i+z[i-l]-1] S[ii+z[il]1],标记为 P a r t ( 3 ) Part(3) Part(3) 部分;
  4. 由蓝色部分一致性可知 P a r t ( 2 , 3 ) Part(2,3) Part(2,3) 两部分完全相同,且 P a r t ( 1 , 2 ) Part(1,2) Part(1,2) 两部分完全相同,则 P a r t ( 1 , 3 ) Part(1,3) Part(1,3) 两部分完全相同;
  5. z z z 数组的定义可知, P a r t ( 1 ) Part(1) Part(1) 部分和 P a r t ( 2 ) Part(2) Part(2) 部分的下一位字符不相同,则 P a r t ( 1 ) Part(1) Part(1) 部分和 P a r t ( 3 ) Part(3) Part(3) 部分的下一位字符不相同,故可得 z [ i ] = z [ i − l ] z[i]=z[i-l] z[i]=z[il]

(情况二):

i − l + z [ i − l ] − 1 > r − l i-l+z[i-l]-1>r-l il+z[il]1>rl(即 i + z [ i − l ] − 1 > r i+z[i-l]-1>r i+z[il]1>r)时, z [ i ] = l + z [ l ] − i z[i]=l+z[l]-i z[i]=l+z[l]i(即 z [ i ] = r − i + 1 z[i]=r-i+1 z[i]=ri+1),超出蓝色部分处需枚举判断,并更新 l = i , r = i + z [ i ] − 1 l=i,r=i+z[i]-1 l=i,r=i+z[i]1

根据情况一所述,可知 P a r t ( 2 , 3 ) Part(2,3) Part(2,3) 两部分的 S [ i − l ∼ r − l ] = S [ i ∼ r ] S[i-l\sim r-l]=S[i\sim r] S[ilrl]=S[ir],而超出 r − l , r r-l,r rl,r 的图中黄色部分就需要枚举来判断是否相同了,若黄色部分中枚举到的位上的字符相同,这时维护的 l , r l,r l,r 就需要更新, l = i , r = i + z [ i ] − 1 l=i,r=i+z[i]-1 l=i,r=i+z[i]1

根据上述两种情况综合来看, z [ i ] = min ⁡ ( z [ i − l ] , l + z [ l ] − i ) z[i]=\min(z[i-l],l+z[l]-i) z[i]=min(z[il],l+z[l]i) 或者 z [ i ] = min ⁡ ( z [ i − l ] , r − i + 1 ) z[i]=\min(z[i-l],r-i+1) z[i]=min(z[il],ri+1),再根据情况枚举即可。

算法思路就是如此了,接下来看一看算法的公式证明。


公式证明

参考博客:扩展 KMP

同样,对于字符串 S S S,我们从下标 0 0 0 开始计数。


结论

对于 i > 0 i>0 i>0,对任意的 0 ≤ l < i 0\le l<i 0l<i 都有:

∀ 0 ≤ x < min ⁡ ( z [ i − l ] , l + z [ l ] − i ) , S [ ( i ) + ( x ) ] = S [ ( 0 ) + ( x ) ] \forall 0\le x<\min(z[i-l],l+z[l]-i),S[(i)+(x)]=S[(0)+(x)] ∀0x<min(z[il],l+z[l]i),S[(i)+(x)]=S[(0)+(x)]


证明

S [ ( i ) + ( x ) ] = S [ ( l ) + ( i + x − l ) ] = S [ ( 0 ) + ( i + x − l ) ] ( i + x − l < z [ l ]    ⟹    x < l + z [ l ] − i ) = S [ ( i − l ) + ( x ) ] = S [ ( 0 ) + ( x ) ] ( x < z [ i − l ] ) \begin{aligned} & \hspace{5mm}S[(i)+(x)] \\ &= S[(l)+(i+x-l)] \\ &= S[(0)+(i+x-l)]{\color{red}(i+x-l< z[l]\implies x<l+z[l]-i)} \\ &= S[(i-l)+(x)] \\ &= S[(0)+(x)]{\color{red}(x<z[i-l])} \end{aligned} S[(i)+(x)]=S[(l)+(i+xl)]=S[(0)+(i+xl)](i+xl<z[l]x<l+z[l]i)=S[(il)+(x)]=S[(0)+(x)](x<z[il])


说明

这个结论说明对于 z [ i ] z[i] z[i] 至少是 min ⁡ ( z [ i − l ] , l + z [ l ] − i ) , ∀ 0 ≤ l < i \min(z[i-l],l+z[l]-i),\forall 0\le l<i min(z[il],l+z[l]i),∀0l<i,这属于是一个递推式的初始化结果,根据上述证明过程不能确保 x ≥ min ⁡ ( z [ i − l ] , l + z [ l ] − i ) , ∀ 0 ≤ l < i x\ge \min(z[i-l],l+z[l]-i),\forall 0\le l<i xmin(z[il],l+z[l]i),∀0l<i 时, S [ ( 0 ) + ( x ) ] ≠ S [ ( i ) + ( x ) ] S[(0)+(x)]\ne S[(i)+(x)] S[(0)+(x)]=S[(i)+(x)],所以还要枚举后续位上的字符更新 z [ i ] z[i] z[i]

在算法过程中,如果我们维护 r = max ⁡ l = 0 i − 1 ( l + z [ l ] ) r=\max_{l=0}^{i-1}(l+z[l]) r=maxl=0i1(l+z[l]),并且只需在枚举后续位上的字符更新 z [ i ] z[i] z[i] 时更新 r r r,则 r r r 被更新最多会有 N N N 次,可以达到 O ( N ) \mathcal{O}(N) O(N) 的时间复杂度。


AC Code

注意:在最后求异或和时要开 long long 类型,会爆 int

参考代码(一)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e7+10;
char s[N<<1],a[N],b[N];
int z[N<<1],len,len_a,len_b;
void z_algorithm()
{
	int l=0;
	for (int i=1;i<len;i++)
	{
		if (l+z[l]>i) z[i]=min(z[i-l],l+z[l]-i);
		while (i+z[i]<len&&s[z[i]]==s[z[i]+i]) z[i]++;
		if (i+z[i]>l+z[l]) l=i;
	}
	z[0]=len_b;
}
int main()
{
	scanf("%s%s",a,b);
	len_a=strlen(a),len_b=strlen(b);
	strcpy(s,b);
	s[len_b]='#';
	strcpy(s+len_b+1,a);
	len=strlen(s);
	//puts(s);
	z_algorithm();
	long ans=0;
	for (int i=0;i<len_b;i++) ans^=1LL*(i+1)*(z[i]+1);
	printf("%lld\n",ans);
	ans=0;
	for (int i=len_b+1;i<len;i++) ans^=1LL*(i-len_b)*(z[i]+1);
	printf("%lld\n",ans);
	return 0;
}

参考代码(二)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e7+10;
char a[N],b[N];
int nxt[N],ext[N];
void z_next()
{
	int len=strlen(b+1);
	nxt[1]=len;
	int l=1,r=1;
	for (int i=2;i<=len;i++)
	{
		if (i<=r) nxt[i]=min(nxt[i-l+1],r-i+1);
		while (i+nxt[i]<=len&&b[1+nxt[i]]==b[i+nxt[i]]) nxt[i]++;
		if (i+nxt[i]-1>r) l=i,r=i+nxt[i]-1;
	}
}
void z_extend()
{
	int len_a=strlen(a+1),len_b=strlen(b+1);
	int l=0,r=0;
	for (int i=1;i<=len_a;i++)
	{
		if (i<=r) ext[i]=min(nxt[i-l+1],r-i+1);
		while (i+ext[i]<=len_a&&ext[i]+1<=len_b&&a[i+ext[i]]==b[1+ext[i]]) ext[i]++;
		if (i+ext[i]-1>r) l=i,r=i+ext[i]-1;
	}
}
int main()
{
	scanf("%s%s",a+1,b+1);
	z_next(),z_extend();
	long long ans=0;
	for (int i=1;i<=strlen(b+1);i++) ans^=1LL*i*(nxt[i]+1);
	printf("%lld\n",ans);
	ans=0;
	for (int i=1;i<=strlen(a+1);i++) ans^=1LL*i*(ext[i]+1);
	printf("%lld\n",ans);
	return 0;
}

End

感谢观看,如有问题欢迎指出。

更新日志

  1. 2025/08/21 开始书写本篇 优快云 博客。
  2. 2025/08/23 本篇 优快云 博客第一次完稿并发布。
  3. 2025/09/06 对本篇 优快云 博客进行了第一次更新,修改了一些单词拼写错误,优化了部分排版形式,优化了一些数学公式表达形式,更改了一些不严谨的表达,更改了文章封面图片。
  4. 2025/09/10 对本篇 优快云 博客进行了第二次更新,在算法思路方面有较大调整,调整了算法思路的逻辑及排版,并且修改了部分语言表述和布局排版。

本篇博客同步于洛谷文章广场,洛谷用户:ZZA000HAH。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HAH-HAH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值