浅谈 exKMP
什么是 KMP?
KMP 是一种高效的字符串匹配算法,通常能在 \(O(n)\) 的线性时间复杂度下求解字符串匹配问题的情况。其精髓在于 border 的求解,也就是我们常说的 \(nxt\) 数组。更多有关 KMP 算法的信息请阅读我的博客文章《浅谈 KMP》以进行一个基本的了解。
虽然但是,exKMP 是扩展 KMP 算法,但它和 KMP 的直接联系并不大——换句话说,你不会 KMP 也能学会 exKMP。
什么是 exKMP?
exKMP。扩展 KMP。恶心 KMP。
exKMP 又称 \(\mathcal{Z}\) 函数、\(\mathcal{Z}\) 算法,与 KMP 类似的,也是一个求解字符串匹配的算法。
定义 \(z_i\) 表示字符串 \(s\) 和字符串 \(s\) 从下标 \(i\) 开始的部分往后这一段的最长公共前缀的长度。嗯,这个和 KMP 中的 border 很像吧,只是不知道这里这个玩意儿叫什么。
exKMP 能够做到在线性 \(O(n)\) 的时间复杂度内求出每个 \(i\) 对应的 \(z_i\),其操作过程与算法 Manacher 类似,可以先阅读我的博客文章《浅谈 Manacher》以进行一个基本的了解。
与 Manacher 同样的,其也维护了一个 \(r\) 最靠右的区间 \([l,r]\)。不同于 Manacher 的回文串,这里仅仅就是根据 \(z\) 算出的一个区间而已,比如 \(i\) 对应的区间就是 \([i,i+z_i-1]\)。
类似的,Manacher 是在纯暴力扩展的基础上加了一些所谓“剪枝”,exKMP 也是一样。exKMP 这里的剪枝又是怎么做的呢?因为当 \(i \le r\) 的时候,根据 \(\mathcal{Z}\) 函数的性质,\(s_{1 \dots r-l+1}\) 与 \(s_{l \dots r}\) 是完全匹配的,即完全相等的,那么 \(z_i\) 就可以直接继承 \(z_{i-l}\) 的答案。但是根据范围限定,得和 \(r-i+1\) 取个 \(\min\),因此就是 \(\min(z_{i-l},r-i+1)\) 了。
这就是 exKMP 中的所谓“剪枝”了,和 Manacher 中的非常相似,是不是?甚至连格式也非常像。接着就在这基础上继续扩展,扩展,直到失配为止。
这就是 exKMP 啦,简直和 Manacher 是兄弟呢。所以 exKMP 和 KMP 的关系到底是?
模版题简析
模版题就是不一样呐,这纯属套板子啊!
这里就和 KMP 相似咯,构造一个字符串 \(s\),让它为 \(b\) 与 \(a\) 的连接(注意是先 \(b\) 后 \(a\) 喔!),中间要插入一个无意义字符以隔开防止错误记录。然后枚举前 \(|b|\) 项算出题目中所谓“\(z\) 的权值”,后 \(|a|\) 项算出题目中所谓“\(p\) 的权值”。记得开 long long,尤其是最后算答案时的那个循环变量 \(i\)。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 4e7+5;
string a,b,s;
LL n,m,k,z[N],ans1,ans2;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void Z_function(){
for(LL i=2,l=0,r=0;i<=k;i++){
z[i]=(i<=r?min(r-i+1,z[i-l+1]):0);
while(s[z[i]+1]==s[z[i]+i])++z[i];
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}z[1]=m;return;
}
int main(){
cin>>a>>b;s=b+"#"+a;
n=a.size(),m=b.size();
k=s.size();s=" "+s;Z_function();
for(LL i=1;i<=m;i++)ans1^=i*(z[i]+1);
for(LL i=1;i<=n;i++)ans2^=i*(z[i+m+1]+1);
cout<<ans1<<"\n"<<ans2<<"\n";
return 0;
}
例题选讲
其实也就两道例题。
CF432D Prefixes and Suffixes
你发现了什么。
你说得对。
完全不需要用 KMP,不知道 KMP 用处在哪里。此题仅需使用 exKMP 一种算法即可解决。
首先我们对 \(s\) 执行一遍 \(\mathcal{Z}\) 函数,容易发现题目要求的这个前缀后缀,不是只要枚举长度 \(i\) 然后判断一下 \(z_{n-i+1}\) 是不是等于 \(i\),不就知道有没有长度 \(i\) 的了嘛!你瞧你瞧 exKMP 多好用。
你会说匹配那一块还是要用 KMP,其实完全没有任何一点必要!我们只需要使用 exKMP 的 \(z\) 数组就可以搞定它。
具体咋滴个搞法嘞?由于能连 \(x\) 个就一定能连 \(x-1\) 个,所以考虑开桶记录所有 \(z_i\) 的值,然后倒序枚举算前缀和,碰上存在的长度 \(i\) 咱就把答案给放进去就好了。最后记得按照这个什么长度 \(i\) 升序输出哟!
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 1e5+5;
string s;
int n,now,cnt,z[N],t[N],ans[N];
vector<int> p;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void Z_function(){
for(int i=2,l=0,r=0;i<=n;i++){
z[i]=(i<=r?min(r-i+1,z[i-l+1]):0);
while(s[z[i]+1]==s[z[i]+i])++z[i];
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}z[1]=n;return;
}
int main(){
cin>>s;n=s.size();s=" "+s;
Z_function();now=n,cnt=0;
for(int len=n;len>=1;len--)
if(z[n-len+1]==len)p.pb(len);
for(int i=1;i<=n;i++)t[z[i]]++;
cout<<p.size()<<"\n";
for(int x:p){
while(now>=x)cnt+=t[now--];
ans[x]=cnt;
}for(int i=1;i<=n;i++)
if(ans[i])cout<<i<<" "<<ans[i]<<"\n";
return 0;
}
CF526D Om Nom and Necklace
噢是的,这个题目真的很有意思。
重述题意,问 \(s\) 中有哪些前缀串满足由 \(k\) 个字符串 \(A\) 再在最后拼接一个字符串 \(B\) 构成,要求 \(B\) 是 \(A\) 的一个前缀子串(可以为空)。
首先枚举所有 \(k\) 的倍数 \(i\)。考虑 \(s\) 的前 \(i\) 个字符构成的前缀是否可以由 \(k\) 个相同子串 \(A\) 拼接而成。这个东西可以通过 KMP 来判断,显而易见的,如果 \(\frac{i}{k}\)(即 \(A\) 的长度)是 \(i-nxt_i\) 的倍数,那么这条要求就是满足的,否则不满足。因此你可以枚举 \(i\) 时判断一下,不满足条件直接给跳过因为它肯定不行。
接下来呢,接下来就要考虑后面那半截 \(B\) 了,这东西和 exKMP 处理的那个 \(z\) 很相似,于是又做一下 exKMP。不难发现前 \(i\) 个字符构成的前缀,到前 \(i+z_{i+1}\) 个字符构成的前缀,都是满足要求的……吗?不对不对,这里没考虑边界情况,\(z_{i+1}\) 可能很大,要是超过了 \(\frac{i}{k}\)(即 \(A\) 的长度)就玩不下去了,因此要对 \(\frac{i}{k}\) 取个 \(\min\)。即,右端点在 \([i,i+\min(z_{i+1},\frac{i}{k})]\) 这个范围内的,都是满足条件的。
不至于还傻乎乎一个个暴力记录吧?这个东西显然是差分吧?然后做一个简单差分最后判断输出就可以啦!
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 1e6+5;
int n,k,nxt[N],z[N],c[N];
string s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void Z_function(){
for(int i=2,l=0,r=0;i<=n;i++){
z[i]=(i<=r?min(r-i+1,z[i-l+1]):0);
while(s[z[i]+1]==s[z[i]+i])++z[i];
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}z[1]=n;return;
}
int main(){
n=read(),k=read();
cin>>s;s=" "+s;
for(int i=2;i<=n;i++){
int j=i-1;
while(j>0){
if(s[nxt[j]+1]==s[i])
{nxt[i]=nxt[j]+1;break;}
j=nxt[j];
}
}Z_function();
for(int i=k;i<=n;i+=k){
if((i/k)%(i-nxt[i]))continue;
c[i]++,c[min(n,i+min(i/k,z[i+1]))+1]--;
}for(int i=1;i<=n;i++)c[i]+=c[i-1];
for(int i=1;i<=n;i++)
cout<<(c[i]?1:0);cout<<"\n";
return 0;
}

浙公网安备 33010602011771号