扩展 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\) 不易,点个赞吧!