z函数|exkmp|拓展kmp 笔记+图解

题外话,我找个什么时间把kmp也加一下图解

\(\LaTeX\):23年11月18号

这个 exkmp 和 kmp 没关系。

本文下标以 \(1\) 开始,\(1\) 开始就不需要进行长度和下标的转换,长度即下标。

定义

给出模板串 \(S\) 和子串 \(T\),长度分别为 \(n\)\(m\),对于每个 \(\text{ans}_i(1\le i\le n)\),求出 \(S_{i\cdots n}\)\(T\) 的最长公共前缀长度。

举个例子:\(S=aabbabaaab\)\(T=aabb\)

\(i=1\) 时,\(S_{1\cdots n}\)\(T\) 的公共前缀是 aabb,长度为 \(4\),即 \(\text{ans}_1=4\)

\(i=2\) 时,\(S_{2\cdots n}\)\(T\) 的公共前缀是 a,长度为 \(1\),即 \(\text{ans}_2=1\)

\(i=3\) 时,\(S_{3\cdots n}\)\(T\) 的公共前缀是 ,长度为 \(0\),即 \(\text{ans}_3=0\)

最终 \(\text{ans}\)4 1 0 0 1 0 2 3 1 0

大家可以自己计算并进行检验。

可以很快想到 \(O(n^2)\) 的做法,但会时超,所以我们要考虑优化,使用转移。

简化条件

我们为了简单,先简化一下条件:\(S=T\),假如 \(S\)\(T\) 完全相同,应该怎么做呢?

我们可以先发现 \(\text{ans}_1=n\)。易得:

代码:

ans[1] = n;

\(\text{ans}_2\) 需要我们的暴力求解,因为下列转移对于 \(S_{1\cdots i}\) 无意义。

代码:

int now = 0;
while(now+2 <= n && S[now+1] == S[now+2]) now++;
ans[2] = now;

我们设目前要求的是 \(k\)。为了通过转移求出 \(\text{ans}_k\),我们要知道目前已求解部分中覆盖最远的下标,该下标范围内一定也是前缀相同的,我们希望这个最远的已求解部分可以覆盖我们的 \(\text{ans}_k\) 部分,这样我们就可以转移出来了。

如图,\(k\) 是我们要求的,\(p_0\) 是已覆盖最远位置的起点,\(p_1\) 是已覆盖最远位置的终点,即 \(p_1=p_0+\text{ans}_{p0}-1\)。就是说,我们要求的 \(\text{ans}_k\) 就在 \(\text{ans}_{p0}\) 中。

我们给 \(S\) 平移 \(p_0\) 位,即:

那么根据最长公共前缀的定义,\(S_{1\cdots (p1-p0+1)}=S_{p0\cdots p1}\),下图中绿色部分相等:

所以,下图中的蓝色部分相等,紫色部分也相等,对应的部分都相等:

那么也就是设有长度 \(l\),使得 \(S_{(k-p0+1)\cdots (k-p0+1+l)}=S_{k\cdots (k+l)}\),即下图褐色部分相同:

假如 \(l=\text{ans}_{k-p0+1}\),这个区间不就是它们的最长公共前缀吗?

又因为最长公共前缀的定义,得知下图褐色 \(1\) 与褐色 \(2\) 相等,上面转移得出褐色 \(2\) 和褐色 \(3\) 相等,进而得出褐色 \(1\) 和褐色 \(3\) 相等。

又因为褐色 \(4\) 和褐色 \(1\) 相等(同一个位置),所以褐色 \(3\) 和褐色 \(4\) 相等:

那么不就得出来了吗?褐色 \(3\)\(\text{ans}\) 就是褐色 \(2\)\(\text{ans}\),即 \(\text{ans}_k=\text{ans}_{k-p0+1}\)

特殊情况

当我们的 \(l\)\(p_1\) 之外,那么就会出现:

尽管紫色部分是相等的,可我们却不知道马赛克部分是否相等,如果相等就比转移后结果大,如果不相等就是转移后结果,所以我们无从可知,只能暴力。

可是如果如此暴力,复杂度又会退化为 \(O(n^2)\),所以我们可以考虑优化:

上图中我们已经得知紫色部分相等,只需暴力 深红色 部分即可。所以我们比对从 \(p_1-k\) 开始。

now = max(0,p1-k); // 因为紫色部分相等,我们选择不遍历这部分,可以省下一些时间,但如果紫色部分不存在,就会出现负下标,在此判断防止负下标的出现
while(now+k <= n && S[now+1] == S[now+k]) now++;
ans[k] = now;
p0 = k;
p1 = k+ans[k]-1;

完整代码

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char S[1010];
int ans[1010];
int n;
int p0, p1;
int main() {
scanf("%s", S+1);
n = strlen(S+1);
ans[1] = n;
int now = 0;
while(now+2 <= n && S[now+1] == S[now+2]) now++;
ans[2] = now;
p0 = 2;
p1 = 2+ans[2]-1;
for(int k = 3; k <= n; k++) {
if(k+ans[k-p0+1]-1 < p1) ans[k] = ans[k-p0+1];
else {
now = max(0,p1-k);
while(now+k <= n && S[now+1] == S[now+k]) now++;
ans[k] = now;
p0 = k;
p1 = k+ans[k]-1;
}
}
for(int i = 1; i <= n; i++) {
printf("%d ", ans[i]);
}
}

为什么是 k+ans[k-p0+1]-1 < p1 而不是 k+ans[k-p0+1]-1 <= p1

\(\text{ans}_{k-p0+1}=0\) 时,会出现 \(p_1=p_0-1\),就有:

那么如果我们单纯判读 k+ans[k-p0+1]-1<=p1\(p_1\) 就无法覆盖 \(k\),从而得出错误结果。

回归原题目

再建一个 \(\text{exans}\),表示 \(S_{i\cdots n}\)\(T\) 的最长公共前缀长度。

\(\text{exans}\) 的第一位还是要暴力,理由同上:

int now=0;
while(now+1 <= m && S[now+1] == T[now+1]) now++;
exans[1] = now;

还记得这张图吗:

模仿求 \(\text{ans}\) 的过程,只不过 \(p_0,p_1\) 表示 \(\text{exans}\) 已求解部分中覆盖最远的地方,即:

在平移 \(p_0\) 位时,我们对应的是 \(T\)

此外均相同,故可以直接复制求 \(\text{ans}\) 的代码了:

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char S[1010],T[1010];
int ans[1010],exans[1010];
int n,m;
int p0,p1;
void qAns() {
ans[1]=m;
int now=0;
while(now+2<=m && T[now+1]==T[now+2]) now++;
ans[2]=now;
p0=2;
p1=2+ans[2]-1;
for(int k=3; k<=m; k++) {
if(k+ans[k-p0+1]-1<p1) ans[k]=ans[k-p0+1];
else {
now=max(0,p1-k);
while(now+k<=m && T[now+1]==T[now+k]) now++;
ans[k]=now;
p0=k;
p1=k+ans[k]-1;
}
}
}
void qExans() {
int now=0;
while(now+1<=n && now+1<=m && S[now+1]==T[now+1]) now++;
exans[1]=now;
p0=1;
p1=p0+exans[p0]-1;
for(int k=2; k<=n; k++) {
if(k+ans[k-p0+1]-1<p1) exans[k]=ans[k-p0+1];
else {
now=max(0,p1-k);
while(now+k<=n && now+1<=m && T[now+1]==S[now+k]) now++;
exans[k]=now;
p0=k;
p1=k+exans[k]-1;
}
}
}
int main() {
scanf("%s %s",S+1,T+1);
n=strlen(S+1);
m=strlen(T+1);
qAns();
qExans();
for(int i=1; i<=n; i++) {
printf("%d ",exans[i]);
}
}

例题

洛谷P5410 【模板】扩展 KMP(Z 函数)

\(\text{ans}\) 的权值与 \(\text{exans}\) 的权值。

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

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long
using namespace std;
char S[20000010],T[20000010];
ll ans[20000010],exans[20000010];
ll n,m;
ll p0,p1;
void qAns() {
ans[1]=m;
ll now=0;
while(now+2<=m && T[now+1]==T[now+2]) now++;
ans[2]=now;
p0=2;
p1=2+ans[2]-1;
for(ll k=3; k<=m; k++) {
if(k+ans[k-p0+1]-1<p1) ans[k]=ans[k-p0+1];
else {
now=max(0LL,p1-k);
while(now+k<=m && T[now+1]==T[now+k]) now++;
ans[k]=now;
p0=k;
p1=k+ans[k]-1;
}
}
}
void qExans() {
ll now=0;
while(now+1<=n && now+1<=m && S[now+1]==T[now+1]) now++;
exans[1]=now;
p0=1;
p1=p0+exans[p0]-1;
for(ll k=2; k<=n; k++) {
if(k+ans[k-p0+1]-1<p1) exans[k]=ans[k-p0+1];
else {
now=max(0LL,p1-k);
while(now+k<=n && now+1<=m && T[now+1]==S[now+k]) now++;
exans[k]=now;
p0=k;
p1=k+exans[k]-1;
}
}
}
int main() {
scanf("%s %s",S+1,T+1);
n=strlen(S+1);
m=strlen(T+1);
qAns();
qExans();
ll z=ans[1]+1,p=exans[1]+1;
for(ll i=2; i<=m; i++) {
z=z^((ans[i]+1)*i);
}
for(ll i=2; i<=n; i++) {
p=p^((exans[i]+1)*i);
}
printf("%lld\n%lld",z,p);
}
posted @   ZnPdCo  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示