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]); } }
例题
求 \(\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); }
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现