前缀函数与Z函数介绍
字符串算法果然玄学=_=
参考资料:
OI Wiki:前缀函数与KMP算法
OI Wiki:Z函数(扩展KMP)
0. 约定#
字符串的下标从 开始。 表示字符串 的长度。
对于字符串 ,记其每一个字符分别为 。
子串 简记为 。特别地,若 ,可记作 ;若 ,可记作 。
对于字符串 , 表示拼接操作,即将字符串 拼接到字符串 之后,构成新的字符串。
记构成的新字符串为 ,则上述拼接操作记为 。
其中符号 表示将 的值赋给 。
不论是字符还是字符串,皆不加引号。
1. 前缀函数#
1.1. 前缀函数的定义#
对于字符串 ,若存在一个非本身的子串 使得 既是 的前缀又是 的后缀,称 为 的一个 。
更加符号化地,对于一个长为 的字符串 ,若存在 使得 且 ,
称 ( 同理) 为 的一个长度为 的 。
对于一个字符串 ,定义 为 的 中最长的。
接下来定义前缀函数:
对于一个字符串 ,定义前缀函数 。
在不引起歧义的情况下,可简记为 ;特别地,定义 。
有时我们关心一整个字符串的前缀函数值,故此时我们也可以记 。
举一个实例:。
1.2. 前缀函数的重要性质#
显然直接根据定义计算前缀函数的时间复杂度是不能接受的,于是我们需要依赖一些有用的性质来加速计算。
主要的性质有下面两个:
- ,且仅当 时有 。
- 令 为 次长的 的长度,则 。
第一个性质是显然的。第二个性质由 自然导出。
1.3. 快速求前缀函数#
这样我们就有了快速求前缀函数的算法:
我们从前向后迭代求。假设我们已经求出了 ,现在要求 。
令 。如果 ,则 。
否则,令 ,重复以上判断直到满足 或 。
若 ,则 。
如此重复直到前缀函数全部计算完成。可以证明,时间复杂度为 。
code:
void prefix()
{
for(int i=1;i<=n-1;i++)
{
int j=pi[i-1];
while(j>0&&s[i]!=s[j])j=pi[j-1];
if(s[i]==s[j])j++;
pi[i]=j;
}
}
1.4. 前缀函数的应用#
1.4.1. KMP#
luoguP3375
一般的方法是通过 优化匹配,这里就不介绍了。
我们有个简单粗暴地多的方法。
假设模式串为 ,文本串为 , 的长度为 , 的长度为 。
构造字符串 ,其中 是 中均不会出现的字符。
然后求 的前缀函数,若,则 在 的 处出现。
总时间复杂度 。代码略。
优化:注意到前缀函数是可以一个一个字符在线处理的,所以空间复杂度可优化到 。
1.4.2 字符串的周期#
luoguP4391
对于字符串 ,若 使 ,则称 是 的周期。
运用 理论,可以发现,若 存在一个长为 的 ,则 是 的周期。
我们应用之前的结果,可知 为 的周期,其中最小的周期为 。
1.4.3 统计每个前缀的出现次数#
根据前缀函数的定义及其性质,以 为右端点有长度为 的前缀。
我们不能按照端点来统计,因为在极端情况下,如 ,时间复杂度达到了平方级别。
但是注意到每一个长为 的前缀的出现中也一定包含长为 的前缀的出现,因此我们可以考虑用较长的前缀的值来更新较短的前缀的值。(具体见代码)
最后别忘了加上每个前缀在初始位置出现的一次。
code:
for(int i=1;i<n;i++)ans[pi[i]]++;
for(int i=n-1;i>0;i--)ans[pi[i-1]]+=ans[i];
for(int i=1;i<=n;i++)ans[i]++;
2. Z函数#
2.1. Z函数的定义#
定义 为字符串 的最长公共前缀。
定义Z函数:
对于字符串 ,定义Z函数 为 的长度。
在不引起歧义的情况下,可简记为 ;特别地,定义 。
根据以上定义,我们有 ;对此我们称 。
有时我们关心一整个字符串的Z函数值,故此时我们也可以记 。
举一个实例:。
2.2. 快速求Z函数#
想要快速求出Z函数的值,我们需要充分发掘Z函数的性质以使信息得到高效利用。
考虑若,即 在 之内:
方便起见,设 。
根据Z函数的定义有 ,则 。
很明显,若 ,则直接有 ;否则,我们能得到 ,想要得出具体数值还需向后枚举。
为了更好地利用上述性质,我们可以在计算过程中维护最大的 。具体操作如下:
初始时令 。
从前到后开始计算。若 ,利用上述性质进行计算。否则,暴力枚举。
如果新的 比之前的更大,即 ,更新 。
code:
z[0]=l=r=0;
for(int i=1;i<n;i++)
{
if(r>i)z[i]=min(z[i-l],r-i+1);//简便起见,我们可以直接让z[i]取为z[i-l]和r-i+1中较小的一个
for(;b[i+z[i]]==b[z[i]];z[i]++);//暴力枚举
if(i+z[i]-1>r){l=i;r=i+z[i]-1;}//更新l,r
}
可以看到每个字符只会被暴力匹配一次,因此复杂度为 。
这个网站有对该算法的演示。
luoguP5410
对于这道题,我们还要求 与 的每一个后缀的 。可以对之前的算法进行修改,也可以直接把 接到 的后面来做。
作者:pjykk
出处:https://www.cnblogs.com/pjykk/p/15012747.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现