学习心得 - 字符串 - Manacher,KMP 及 Z 函数(扩展 KMP,exKMP)
序
鉴于之前写文章时没有怎么写 KMP 的,这里也会一并记录一下 KMP 的学习笔记。这两个算法其实没啥太大的关系,但是又有差不多的工作原理。而 Z 函数倒是更类似于 Manacher 的算法。下面我会用尽可能容易理解的语言来讲解一下这两个字符串算法。之后我发现可以用 KMP 串起整个提高级字符串算法,因此写下此文。
此文可以用作提高级字符串全家桶学习笔记。
Manacher
由于 Manacher 的算法工作过程和 Z 函数基本是完全一致的,这里会用尽可能简单的语言稍微讲讲 Manacher。
Manacher 本质是一个均摊分析的暴力,从 #,首尾增加 < 与 > 表示开头与结束。然后我们考虑暴力。暴力可以直接枚举中心点,然后向左右两边贪心扩展,复杂度
接下来就是 Manacher 的主要算法。其借助一个优化条件:回文中心对称。对于一个点
那么这就是 Manacher 的全部内容,如果不理解可以看我的往期文章,或者私信 Ivan422。
KMP
既然要介绍 exKMP,首先你得先理解 KMP 是什么。KMP 是一种用于处理字符串内匹配的算法。还是首先考虑暴力,那么就是类似滑动窗口的匹配算法,哈希可以做到差不多
接下来我们要介绍一个东西叫做 Border,也叫做最长公共前后缀,如
此时你发现:失配的位置永远算是一个后缀(从失配位置往后的所有都算作是失配)。那么此时你考虑如何移动。显然此时前面都是匹配清楚的。你会发现,不存在一个中间开始的值可以开始匹配,因为它一开始就断了匹配。那么此时我们就可以把前缀移动到失配的位置,看看能不能有更多的可能。因此,我们对模式串计算
:::info[
根据我刚刚的描述,你可能会认为
注意:最长公共前后缀
:::
我们先不考虑如何求
:::warning[关于 Border 回退的写法区分]{open}
我查询了一些资料,这一段有非常多写法,有的是
:::
此时,如果我们跳完 Border 了,那么有两种情况:啥都没找到,
这一段的编码其实不难,但是很难理解。这是我很久以前的一份代码,说的其实差不多都对,但是可能是从题解里稍微改了点,没有真正的理解。
j=0; // 再从头匹配
for(int i=0;i<a.size();i++){
while(j>0&&a[i]!=b[j])j=bor[j-1]; // 开始真正的匹配, 失配, 跳过到可匹配点
if(a[i]==b[j])j++; // 模式串匹配成功, 更新 border 指针, 找到答案
if(j==b.size()){cout<<i+1-b.size()+1< <endl;j=bor[j-1];} // 正好匹配完, 输出位置, 但是要直接跳过
}
那么我们考虑如何求模式串的最长公共前缀。你会发现最长公共前后缀的本质是什么:
设我们当前处理到黑色的部分,则其最长公共前后缀为下划线部分,本质是一个自己匹配自己的过程。这句话看起来很抽象,但是如果一个串的开头和结尾相同,则你可以复制一个完全一致的段贴到其结尾的部分,本质就是最长公共前后缀。然后还有一个问题:匹配自己需要
实现略微有点不一样,还是一样的
代码和我说的是一样的,但是可能与大众的做法不一样。
j=0; // border 指针, 从头开始
for(int i=1;i<b.size();i++){
while(j>0&&b[i]!=b[j])j=bor[j-1]; // 模式串失匹配, 跳过, 用上一位的, 跳过当前位置
if(b[i]==b[j])j++; // 模式串匹配成功, 更新 border 指针
bor[i]=j; // border 数组
}
:::warning[关于 Border 记录方法的区分]{open}
同上,这里会稍微讲讲别的做法。有一种
总结一下区别吧:
你发现两处差(
:::
最终会给一份我重构的带
:::success[KMP 模板代码]
使用说明:将 #define print_border 的注释删掉可以输出 Border,将 #define index 1 改成 #define index 0 可以切换为 _0_index_ 的运行方式,否则是 _1_index_ 运行。
#include<bits/stdc++.h>
using namespace std;
#define index 1
// #define print_border
namespace KMP{
const int N=1e6+10;
int n,m;
int border[N]; // 最长公共前缀(有时也记作 next)
// 0-index 的 KMP 写法
void _0_index_(string s,string t){
n=s.size();
m=t.size();
// t 自匹配部分
int j=0; // border 指针
#ifdef print_border
cout<<"Border["<<0<<"] = "<<border[0]<<"\n";
#endif
for(int i=1;i<m;i++){
while(j&&t[i]!=t[j]) j=border[j-1];
if(t[i]==t[j]) ++j;
border[i]=j;
#ifdef print_border
cout<<"Border["<<i<<"] = "<<border[i]<<"\n";
#endif
}
// s 匹配 t 部分
j=0; // border 指针
for(int i=0;i<n;i++){
while(j&&s[i]!=t[j]) j=border[j-1];
if(s[i]==t[j]) ++j;
if(j==m){ cout<<i+1-m+1<<"\n"; }
}
// 符合此题的输出,非必须
for(int j=0;j<m;j++) cout<<border[j]<<" ";
}
// 1-index 的 KMP 写法
void _1_index_(string s,string t){
n=s.size();
m=t.size();
s=" "+s;
t=" "+t;
int j=0; // 注意这里的意义不一样了,这是已经匹配的位置
// t 自匹配部分
#ifdef print_border
cout<<"Border["<<1<<"] = "<<border[1]<<"\n";
#endif
for(int i=2;i<=m;i++){
while(j&&t[i]!=t[j+1]) j=border[j];
if(t[i]==t[j+1]) ++j;
border[i]=j;
#ifdef print_border
cout<<"Border["<<i<<"] = "<<border[i]<<"\n";
#endif
}
// s 匹配 t 部分
j=0; // border 指针
for(int i=1;i<=n;i++){
while(j&&s[i]!=t[j+1]) j=border[j];
if(s[i]==t[j+1]) ++j;
if(j==m){ cout<<i-m+1<<"\n"; }
}
// 符合此题的输出,非必须
for(int i=1;i<=m;i++) cout<<border[i]<<" ";
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
string s,t;
cin>>s>>t;
if(index==0)
KMP::_0_index_(s,t);
else
KMP::_1_index_(s,t);
return 0;
}
:::
时间复杂度分析:因为跳
扩展 KMP / Z 函数 / exKMP
:::warning[关于代码的
为适应尽可能多的字符串问题,Z 函数一章如果没有解释,则全部字符串均为
模板题中定义的
:::
名字老多了!但是跟 KMP 一样让人难以理解,不知道为什么之前没有记过笔记。这个东西的功能很怪异,其能在
为什么这个东西叫做
这个东西和
是可以的。我们考虑类似 Manacher 维护一个
然后根据 Manacher 的结论,我们发现
这一段的代码其实很简单:
void exKMP(string s,int n){
int l=1,r=0;
for(int i=1;i<=n;i++)
Z[i]=0;
Z[1]=n;
for(int i=2;i<=n;i++){
if(i>r)
Z[i]=0;
else{
int k=i-l+1;
Z[i]=min(Z[k],r-i+1);
}
while(i+Z[i]<=n&&s[Z[i]+1]==s[i+Z[i]])
++Z[i];
if(i+Z[i]-1>r)
l=i,
r=i+Z[i]-1;
}
}
但是问题来了,这个东西跟 KMP 有任何说明关系吗?到这里还没有,但是我们考虑怎么匹配。由于你发现这个模式串的
一个合法的匹配就是
:::info[位置的具体关系]{open}
首先,根据定义我们可以有:
同理,我们能找到一个:
因为
所以我们可以得到:
同理根据字符串的对应关系,我们有:
这个
你发现
此时设
符合我们要的问题式,则得到
:::
那么我们得到了一个可能的最大化取值:
void exKMP2(string s,int n,string t,int m){
exKMP(t,m);
int l=1,r=0;
for(int i=1;i<=n;i++)
p[i]=0;
for(int i=1;i<=n;i++){
if(i>r)
p[i]=0;
else{
int k=i-l+1;
p[i]=min(Z[k],r-i+1);
}
while(i+p[i]<=n&&t[p[i]+1]==s[i+p[i]])
++p[i];
if(i+p[i]-1>r)
l=i,
r=i+p[i]-1;
}
}
最终得到的答案就是