扩展 KMP——Z 函数
本文建议进行适当缩小以适应 \(\KaTeX\) 行间公式。
本文符号规定与 \(\downarrow\) 文相同。
建议:前置知识(KMP)。
\(\texttt{Upd 24.2.4}\) 修正(增删)描述。简洁多了。
引入
写本文的目的是让不了解扩展 KMP 的人快速(写完发现似乎无法那么快)理解。(作者在学扩展 KMP 时可谓焦头烂额,于是搞懂后便写下此文。)
文章较为复杂,有错请指出。
请一定静下心看完!理解了没有那么复杂!
扩展 KMP(Z 函数)
我们已经认识了前缀函数了。它是维护 一个字符串的所有前缀的 最长公共真前后缀的 长度——
其中两段被括起来的子串相等。而 Z 函数——
看出区别了吗?前缀函数是在 \([0,i]\) 区间向内“扩张”,Z 函数则是都向右“扩张”。图 \((2)\) 中被大括号括起的子串(一共两个但相等)叫匹配段(Z-box)。
形式化地说,\(z_i\) 是满足 \(s[0\dots x-1]=s[i\dots i+x-1]\) 的 \(x\) 中最大的一个。注意它俩的长度均为 \(z_i\)。
如何计算 Z 函数?
- 需要维护一个区间 \([l,r]\),满足它是当前已知的匹配段中 \(r\) 最大的一个。我们维护它的目的是利用它来加速暴力算法。(暴力算法指朴素地枚举 \(x\)。)在计算 \(z[i]\) 时我们保证 \(l\le i\)。初始时 \(l=r=0\)。
- 然后枚举每一个 \(i\) 计算。
首先注意到既然 \([l,r]\) 是匹配段,必然满足 \(s[0\dots r-l]=s[l\dots r]\)(根据匹配段的定义):
分类讨论:
若 \(\LARGE r<i\)
那么由于 \([l,r]\) 与 \([i,n)\) 不相交,\([l,r]\) 就没有了利用价值,舍去,暴力计算 \(z_i\) 的值。
同时 \([l,r]\gets [i,i+z_i-1]\)。
反之若 \(\LARGE i\le r\)
注意到 \(s[i-l\dots r-l]=s[i\dots r]\)。它们分别是 \(s[0\dots r-l],s[l\dots r]\) 的子串:
即“\(\text{\color{red}these}\)”段一定相等。
如果 \(\Large z_{i-l}<r-i+1\)
即“\(\text{\color{red}these}\)”段比“\(\text{\color{blue}these}\)”段长,图示(无法全部显示可以浏览器缩小):
- 首先由于两个“\(\text{\color{blue}these}\)”段是 \(i-l\) 的匹配段,故相等。
- 其次由于两个“\(\text{\color{red}these}\)”段都是匹配段 \([l,r]\)(最大的黑色大括号)的子串,也相等。
- 再次由于“\(\text{\color{red}these}\)”段同时包含了“\(\text{\color{blue}these}\)”段、“\(\text{\color{green}these}\)”段,且位置、长度相同:
可得“\(\text{\color{blue}these}\)”段 \(=\) “\(\text{\color{green}these}\)”段。 - 显然 \({\color{lime}s_{i-l+z(i-l)}=s_{i+z(i-l)}}\)(都在“\(\text{\color{red}these}\)”段相同位置)。
- 由于 \(i-l\) 的匹配段仅为 “\(\text{\color{blue}these}\)”段,也即匹配分别在 \({\color{purple}s_{z(i-l)}},{\color{lime}s_{i-l+z(i-l)}}\) 处停止,可得它俩不相等(所以才停止匹配)。
- 有上 \(2\) 条可得 \({\color{purple}s_{z(i-l)}}\ne {\color{lime}s_{i+z(i-l)}}\)。
- 观察发现上一条等价于 \(i\) 处匹配会在 \({\color{purple}s_{z(i-l)}},{\color{lime}s_{i+z(i-l)}}\) 处停止。即 \(z_i=z_{i-l}\)。
总结:当 \(z_{i-l}<r-i+1\) 时:\(z_i=z_{i-l}\)。
反之如果 \(\Large z_{i-l}\ge r-i+1\)
即“\(\text{\color{red}these}\)”段不比“\(\text{\color{blue}these}\)”段长,那么:
- 首先两个“\(\text{\color{red}these}\)”段相等、两个“\(\text{\color{blue}these}\)”段相等。
- 这回我们无法得到“\(\text{\color{blue}these}\)”段、“\(\text{\color{green}these}\)”段相等了,因为它们没有被“\(\text{\color{red}these}\)”段包含。
- 但是,反之我们可以得到“\(\text{\color{orange}these}\)”段、“\(\text{\color{red}these}\)”段相等。它们都有被“\(\text{\color{blue}these}\)”段包含,且位置、长度相同。
- 那么“\(\text{\color{orange}these}\)”段、右边的“\(\text{\color{red}these}\)”段就组成了一个匹配段,所以至少 \(z_i=r-i+1\),不排除更长,所以要朴素更新。
总结:当 \(z_{i-l}\ge r-i+1\) 时:\(z_i\gets r-i+1\),并需要朴素延长。
最终逻辑
- 当 \(r<i\) 时,\(z_i\gets0\),然后暴力尝试增加 \(z_i\)。\((1)\)
- 反之当 \(i\le r\) 时:
- 若 \(z_{i-l}<r-i+1\),\(z_i=z_{i-l}\)。\((2)\)
- 反之若 \(z_{i-l}\ge r-i+1\),\(z_i\gets r-i+1\),然后暴力尝试增加 \(z_i\)。\((3)\)
- 注意到 \((1),(3)\) 的暴力增加是可以合并的。
代码实现
题目:P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷
#include <iostream> using namespace std; const int N=20114514; string a,b; int n,m; int z[N*2]; void calc_Z(string& s,int len) { z[0]=len; for(int i=1,l=0,r=0;i<len;i++) { z[i]=0; if(i<=r&&z[i-l]<r-i+1)// (2) { z[i]=z[i-l]; } else { z[i]=max(0,r-i+1);// (1),(3) while(i+z[i]<len&&s[z[i]]==s[i+z[i]])// 暴力增加 { z[i]++; } } if(i+z[i]-1>r)// 试图更新 [l,r] { l=i; r=i+z[i]-1; } } } inline void exKMP() { // m 1 n string tmp=b+"#"+a;// 经典的拼接 calc_Z(tmp,tmp.length()); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); cin>>a>>b;// a 中匹配 b n=a.length(),m=b.length(); calc_Z(b,m); long long ans=0;// 不开 long long 见祖宗 for(int i=0;i<m;i++) { ans^=(1ll*(i+1)*(z[i]+1));// 统计答案 } printf("%lld\n",ans); exKMP(); ans=0; for(int i=0;i<n;i++) { ans^=(1ll*(i+1)*(z[m+1+i]+1));// 统计答案 } printf("%lld",ans); return 0; }
后记
本文应该是笔者写的最认真的文章了,因为理解时很痛苦。
网络上也有很多其他优秀的介绍,也可以参考。
最后:画 \(\KaTeX\) 不易,点个赞吧!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程