扩展 KMP,Z 函数
引流前言
学完这些有助于你更好理解 Zfunc,甚至是一看就会!
1. 是什么
首先说说最长公共前缀。
不是感觉没什么好说的啊,复制下百科吧。
不是怎么百科搜不到啊。。。。
我们让 \(LCP(s,t)\) 表示字符串 \(s,t\) 的所有相同前缀中,最长的那一个。
LCP 就是 Longest Common Prefix 的 abbr.
扩展 KMP 算法,又称 Z 函数(Zfunc) 就是让你对于字符串 \(s\) 的每一个后缀 \(s_i\),求出 \(LCP(s,s_i)\)。
我猜你会和我一样马上想到二分哈希。
xde :想到这个东西的人真是脑子里有包
你说得对二分哈希是 \(O(n\log n)\) 的,但是求 Zfunc 是可以做到 \(O(n)\) 的。
怎样球呢?
球球球。。
2. 怎么算
当然要尝试用之前算过的东西去更新现在要算的东西。
Manacher 和 KMP 点了个赞。
这里我们让 \(z\) 的下标从 \(0\) 开始。
考虑记录之前求解的 \(z_{i-1}\) 中,\(i+z_i-1\) 最大的那一个,即右端点最靠右的匹配段。我们记 \(l=\arg \max\{j+z_j-1\},r= \max \{j+z_j-1\}\),初始时 \(l=0,r=0\)。
现在我们要求解 \(z_i\) 了。分情况:
-
如果 \(i<=r\)
那么 \(s[i,r]=s[i-l,r-l]\),这个东西就是 LCP 性质了,手摸一下不难得出。
学这种东西不要懒得画画啊,勤奋画一画就懂了。
现在考虑 \(z_{i-l}\) 会怎样贡献到 \(z_i\)
-
如果 \(z[i-l] \le r-i+1\),也就是说如果直接继承 \(z[i-l]\) 不会超出 \(r\) 的右边界,因为 \(r\) 之后的东西关于 \(s\) 的 LCP 我们是未知的。因此我们可以直接继承,也就是:
z[i]=z[i-l]; -
如果 \(z[i-l] > r-i+1\),我们只能让它继承到 \(r-i+1\)。之后只能让它自己往后扩展了。因为 \(r\) 之后的东西关于 \(s\) 的 LCP 我们是未知的。
-
-
如果 \(i>r\)
那么再画个图你就发现这种情况怎么都无法贡献给 \(i\) ,于是只能让它自己往后扩展了。
此外,如果 \(r<i+z_i-1\),我们要更新 \(l,r\)。
然后就有模板啦:
for(int i=1,l=0,r=0;i<len;i++){
if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
else{
z[i]=max(0,r-i+1);// 这里把后两种情况合并到一起去了
while(i+z[i]<len&&b[z[i]]==b[i+z[i]])z[i]++;
}
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
3. 怎么用
这东西应用还是挺好玩的。
P5410 【模板】扩展 KMP/exKMP(Z 函数)
如果你对一个模式串求出了它的 Zfunc,像 KMP 一样,你也可以求出任意一个主串对这个模式串的 Zfunc。
类似我们求一个 Zfunc 的过程,但是继承时不应继承正在求的这个串的 \(z\),因为这个串之前的 \(z\) 不能正确的贡献自己,只有匹配的模式串的 \(z\) 才可以。
上面这句话的意思是把 if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l]; 改成 if(i<=r&&z2[i-l]<r-i+1)z1[i]=z2[i-l];。
另外,在进行 while 循环的暴力扩展时,注意 \(z\) 要同时小于两个串的长度,判断下即可。
这里还是画个图就能明白的事,一定要去画个图啊!
于是把上面的代码结合起来,你可以通过模板题了。
code
Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
ll x=0,f=1;
char c=getchar();
while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
const int N=2e7+5;
int z1[N],z2[N];
int main(){
string a,b;cin>>a>>b;
int len1=a.size(),len2=b.size();
int len=b.size();
z2[0]=len2;
for(int i=1,l=0,r=0;i<len2;i++){
if(i<=r&&z2[i-l]<r-i+1)z2[i]=z2[i-l];
else{
z2[i]=max(0,r-i+1);
while(i+z2[i]<len&&b[z2[i]]==b[i+z2[i]])z2[i]++;
}
if(r<i+z2[i]-1)l=i,r=i+z2[i]-1;
}
int p1=0,p2=0;
while(a[z1[0]]==b[z1[0]])z1[0]++;
for(int i=1,l=0,r=0;i<len1;i++){
if(i<=r&&z2[i-l]<r-i+1)z1[i]=z2[i-l];
else{
z1[i]=max(0,r-i+1);
while(i+z1[i]<len1&&z1[i]<len2&&b[z1[i]]==a[i+z1[i]])z1[i]++;
}
if(r<i+z1[i]-1)l=i,r=i+z1[i]-1;
}
ll ans1=0,ans2=0;
for(int i=0;i<len2;i++)ans1=ans1^(1ll*(i+1)*(z2[i]+1));
for(int i=0;i<len1;i++)ans2=ans2^(1ll*(i+1)*(z1[i]+1));
cout<<ans1<<'\n'<<ans2;
return 0;
}
当然不止这一种做法,我们用字符串匹配题的一个常用技巧:把两个串拼起来。
本题中,我们让 \(b=b+a\),加号表示字符串拼接。
现在,我们直接求解拼接好的 \(b\) 的 Zfunc,于是你会发现:
好像不对啊,由于把两个字符串拼起来算,因此两个数组不能越界,例如计算 \(b\) 串 \(i\) 位置的 \(z_i\) 时,应满足 \(i+z_i < size(b)\)。
当然我的第一个想法是在 for 循环里面加个越界限制:
for(int i=1,l=0,r=0;i<len;i++){
if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
else{
z[i]=max(0,r-i+1);
while(i+z[i]<len&&b[z[i]]==b[i+z[i]])++z[i];
}
if(i<len2)z[i]=min(z[i],len2-i);// 这里
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
但是这样你会 TLE #7,12。
但是其实我们不需要管这个越界,只需要在查询答案时取个最小值就好了。
为什么呢?
注意到越界本身对计算前缀这种东西没有影响(即使从 \(b_i\)
开始的 lcp 跑到了 \(a\) 的范围里去,它依然是和 \(b\) 的 lcp,可以参与对各 \(a_i\) 的贡献),因此我们只需在统计答案时取个最小值即可,不需要 for 循环里的那一句话。
下面是代码:
Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
ll x=0,f=1;
char c=getchar();
while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
const int N=4e7+5;
int z[N];
string a,b;
int main(){
ios::sync_with_stdio(0);
cin.tie();
cin>>a>>b;
int len1=a.size(),len2=b.size();
b=b+a;
z[0]=len2;
int len=b.size();
for(int i=1,l=0,r=0;i<len;++i){
if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
else{
z[i]=max(0,r-i+1);
while(i+z[i]<len&&b[z[i]]==b[i+z[i]])++z[i];
}
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
z[0]=len2;
ll ans1=0,ans2=0;
for(int i=0;i<len2;i++)ans1=ans1^(1ll*(i+1)*(min(z[i],len2-i)+1));
for(int i=len2;i<len;i++)ans2=ans2^(1ll*(i+1-len2)*(min(z[i],len2)+1));
cout<<ans1<<'\n'<<ans2;
return 0;
}
加入 IO 优化后,方法 1 要略快于方法 2。
P2375 [NOI2014] 动物园
这题有用 KMP Border 理论的 \(O(nL)\) 优秀算法,但是我还有一个用 Zfunc 的 \(O(nL)\) 算法。
首先对原串算出它的 Zfunc。
考虑每个 \(z_i\),你会发现既是它的后缀又是它的前缀还不能重叠这样的串在 \(i\) 位置就是 \(s[1,1]\) 和 \(s[i,i]\),\(i+1\) 就是 \(s[1,2]\) 和 \(s[i,i+1]\)。也就是这个东西会贡献给 \(i\) 及其之后的一些位置一个串的答案。
考虑这个位置是什么,如果 \(i\notin[1,z_i]\),那么显然 \([i,i+z_i-1]\) 的位置都是可以贡献到的,因为这些前缀后缀一定不会重叠。
但是如果 \(i\in[1,z_i]\),那么就只能贡献到 \([i,i+(i-1)-1]\) 的位置了,因为 \([1,i]\) 和 \([i,i+z_i-1]\) 是有重合的,这样在此及其后的所有串都是不合法的,无法被贡献。
于是问题变成了一堆区间加和一次全局求和,这个东西差分数组就可以。时间复杂度是 \(O(nL)\)。
下面的 code 用了树状数组,我也不知道我当时为什么要写树状数组。
Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
ll x=0,f=1;
char c=getchar();
while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
const int N=1e6+6;
const int mod=1e9+7;
int nxt[N],z[N];
int c[N];
int m;
void add(int p,int k){for(;p<=m;p+=(p&(-p)))c[p]+=k;}
ll pf(int r){
ll sum=0;
for(;r>0;r-=(r&(-r))){sum+=c[r];}
return sum;
}
int main(){
int n;cin>>n;
while(n--){
string s;cin>>s;
m=s.size();
memset(nxt,0,sizeof nxt);
memset(z,0,sizeof z);
memset(c,0,sizeof c);
for(int i=1,l=0,r=0;i<m;i++){
if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
else{
z[i]=max(0,r-i+1);
while(i+z[i]<m&&s[z[i]]==s[i+z[i]])z[i]++;
}
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
ll ans=1;
for(int i=1;i<=m;i++){
int lbc=min(i-1,z[i-1]);
add(i,1);add(i+lbc,-1);
}
for(int i=1;i<=m;i++)ans=ans*(pf(i)+1)%mod;
cout<<ans<<'\n';
}
return 0;
}
写完力!
有什么好题一会单独开个好题选讲。

浙公网安备 33010602011771号