扩展KMP
扩展KMP
所有字符串下标从1开始
有一天,你虐OJ的时候遇到了这道题
ExKMP
给定两个字符串,你要求出两个数组:
的 函数数组 ,即 与 的每一个后缀的 LCP 长度。 与 的每一个后缀的 LCP 长度数组 。 对于一个长度为
的数组 ,设其权值为 。 求
的权值。
对于的数据, ,所有字符均为小写字母。
扩展KMP即为解决这类问题的方法,具体如下:
设
Z函数
即题目中的
显然,
设我们已经求解
此时呢,根据
的定义,有 。可以看作字符串 向后平移了 个单位长度。那么由于 ,则 也是由 平移过来的,此时
需要注意的是,最长扩展长度不能超过
。先令 while(i+z[i]<=n&&a[i+z[i]]==a[z[i]+1])++z[i];
。这样的话,若
,直接暴力进行匹配。
每次匹配完之后需要更新
由于内层的while
执行多少次,就会使得
void Z(){
z[1]=n;
for(int i=2,l=0,r=0;i<=n;i++){
if(i<=r)z[i]=min(z[i-l+1],r-i+1);
while(i+z[i]<=n&&a[i+z[i]]==a[z[i]+1])++z[i];
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
}
数组
类比计算
void ExKMP(){
Z();
for(int i=1,l=0,r=0;i<=m;i++){
if(i<=r)p[i]=min(z[i-l+1],r-i+1);
while(i+p[i]<=m&&b[i+p[i]]==a[p[i]+1])++p[i];
if(r<i+p[i]-1)l=i,r=i+p[i]-1;
}
}
正确性的证明:
在
同样是暴力匹配,暴力更新
Complete template
char a[N],b[N];
int n,m,z[N],p[N];
void Z(){
z[1]=n;
for(int i=2,l=0,r=0;i<=n;i++){
if(i<=r)z[i]=min(z[i-l+1],r-i+1);
while(i+z[i]<=n&&a[i+z[i]]==a[z[i]+1])++z[i];
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
}
void ExKMP(){
Z();
for(int i=1,l=0,r=0;i<=m;i++){
if(i<=r)p[i]=min(z[i-l+1],r-i+1);
while(i+p[i]<=m&&b[i+p[i]]==a[p[i]+1])++p[i];
if(r<i+p[i]-1)l=i,r=i+p[i]-1;
}
}
signed main(){
cin>>b+1>>a+1;
n=strlen(a+1),m=strlen(b+1);
ExKMP();
int ans=0;
for(int i=1;i<=n;i++)ans^=i*(z[i]+1);
cout<<ans<<"\n";ans=0;
for(int i=1;i<=m;i++)ans^=i*(p[i]+1);
cout<<ans<<"\n";
}
事实上,我们可以将两个串合并,并以一个特殊字符如&
隔开,跑一遍
ExKMP算法是KMP算法的扩展,这意味着它可以做很多KMP可以做和不可以做的事情
应用
字符串匹配
给定串
解法1
上文的
for(int i=1;i<=m;i++){
if(p[i]==n){
cout<<"Appear: "<<i<<"\n";
}
}
解法2
同样的,我们也可以将
循环元问题
根据KMP的知识,我们知道长度为
考虑食用
类比 KMP ,我们考虑如何判断
事实上,
所以为循环元的充要条件是:
最小循环元就倒序枚举
Z();
for(int i=n;i;--i)
if(i+z[i]-1==n&&z[z[i]+1]==i-1){
cout<<n/z[i]<<"\n";break;
}
这种方法的本质上枚举最后一次循环位置,以
本质上是通过
还有一种方法,枚举的是循环节,来判断是否合法,会简洁一些。
也即枚举循环节
前后缀问题
也即:求
在KMP算法中,我们通过不断跳
既是前缀又是后缀的子串,换句话说就是:
找到所有符合要求的串,输出即可。
for(int i=n;i;--i)
if(i+z[i]-1==n)
cout<<z[i]<<" ";
cout<<"\n";
回文串问题
对于每个字符串
,求出一个字符串 , 需要满足:
为 的前缀; 是一个回文字符串; 应尽可能小;
这个问题很有意思。我们判断一个回文串有一个方法是将其翻转并拼在原串前,用特殊符号隔开,最后看
对于
问题等价于求出后缀中最长的回文串。
根据回文串的判断方式,我们设
应用
NOIP2020 字符串匹配
说一下我做的时候的心路历程
容易发现
表示串 出现奇数次的字符数量 表示在前 个字符中出现奇数次字符数量为 的数的个数 表示在前 个字符中出现奇数次字符数量 的数的个数
inline void init(){
cin>>s+1;
n=strlen(s+1);
for(re int i=n;i;--i){
t[i]=t[i+1];
cnt[s[i]-'a']++;
if(cnt[s[i]-'a']&1)t[i]++;
else t[i]--;
}
memset(cnt,0,sizeof cnt);
for(re int i=1;i<=n;++i){
a[i]=a[i-1];
cnt[s[i]-'a']++;
if(cnt[s[i]-'a']&1)a[i]++;
else a[i]--;
}
memset(cnt,0,sizeof cnt);
for(re int i=1;i<=n;i++){
for(re int j=0;j<26;++j)g[i][j]=g[i-1][j];
g[i][a[i]]++;
}
for(re int i=1;i<=n;i++){
f[i][0]=g[i][0];
for(re int j=1;j<26;j++)f[i][j]=f[i][j-1]+g[i][j];
}
}
然后考虑枚举
inline void solve(){
re long long ans=0;
for(re int i=n-1;i;--i){
ans+=f[i-1][t[i+1]];
if(i%(i-nxt[i])==0){
int x=i-nxt[i];
for(int j=1;j*x<i;j++){
int len=x*j;
if(i%len)continue;
ans+=f[len-1][t[i+1]];
}
}
}
cout<<ans<<"\n";
}
这时候你就会惊奇的发现,交上去只有 48pts
。
考虑优化。这个做法在循环节么次都是1的情况下会爆成
换一个角度呢?我可以枚举
显然是可以的,仅需要判断其最多能够循环到哪个位置。这时候需要用上
设
更详细地说,这个循环节一直循环到 n-1
取较小值。所以代码应该是:
inline void solve(){
re long long ans=0;
for(re int i=2;i<n;++i){
int ed=i+z[i+1];ed=min(ed,n-1);
for(int j=1;j*i<=ed;++j){
ans+=f[i-1][t[i*j+1]];
}
}
cout<<ans<<"\n";
}
多测不清空,抱灵两行泪
这个做法会被卡到92(洛谷),需要氧气。
参考文献:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!