更新日志
2024/12/21:开工。
作用
KMP算法本质作用是求字符串前缀的最长 border。
border:同时是一个字符串前缀和后缀的字符串,称为前者的 border。
常见的,我们可以使用它进行字符串匹配。
思路
假如我们要在 \(s_1\) 中匹配 \(s_2\)。
我们使用 nxt
数组储存 \(s_2\) 所有前缀的 border。
假如当前这一位两个字符串失配,也就是字符匹配不上,我们就需要找到下一个 \(s_2\) 前缀来匹配。
如果是暴力算法,那么我们每次都会重新从 \(s_2\) 的开头匹配,但 KMP 可以找到更近的可行位置。
假如 \(s_2\) 的第 \(j\) 位失配了,那么 \([1,j-1]\) 必然是匹配上的,那么我们需要找到的最近的可行位置,它前面的部分必然也是匹配上了的。
形式化的说,假如 \(i\) 的前一个可行位置为 \(j\),则 \(s[1,j-1]=s[i-j+1,i-1]\)。
将那么, \(s[1,j-1]\) 就是 \(s[1,i-1]\) 的一个 border。
故而我们只需要找出每个前缀的最大 border,就可以求出下一个匹配位置了。
模板
前言
KMP算法有两种常见实现方式。这里的实现方式并不是指代码写法,而是指 nxt
数组内涵
它们分别有各自特定的用处与优势。
事实上二者是通用的,只不过在各自方面代码稍微简单一些而已。
下面默认字符串从 \(1\) 开始编号。
border重心
这个写法中,nxt
数组储存了 \([1,i]\) 的 border 长度。
这样可以很方便地处理后续与 border 有关的操作,但是求 nxt
与匹配时会略微复杂。
一般情况下,除非题目要求 border,还是用匹配重心更方便些。
求nxt
int nxt[N];
void getnxt(string &s){
int i=1,j=0;
nxt[0]=-1;
while(i<s.size()){
if(j&&s[i]!=s[j])j=nxt[j-1]+1;
else nxt[i++]=j++;
}
}
单次查找
如果当前第 \(|s_2|\) 位也已完成匹配,整个 \(s_2\) 就匹配完成了。说明是子串
bool kmp(string &s1,string &s2,int nxt[]){
int i=1,j=1;
while(i<s1.size()){
if(j&&s1[i]!=s2[j])j=nxt[j-1]+1;
else i++,j++;
if(j==s2.size())return 1;
}
return 0;
}
全部位置
当前匹配完后,此时 \(j\) 指向的是空字符,下一步必然失配,所以可以直接这么写。
void kmp(string &s1,string &s2,int nxt[],vec<int> &ans){
int i=1,j=1;
while(i<s1.size()){
if(j&&s1[i]!=s2[j])j=nxt[j-1]+1;
else i++,j++;
if(j==s2.size())ans.pub(i-j+1);
}
}
匹配重心
在这种写法中,nxt
储存 \([1,i-1]\) 的 border,这样求 nxt
和匹配都会变得更简洁。
但如果有 border 有关操作就会变得复杂。
求nxt
int nxt[N];
void getnxt(string &s){
int i=1,j=0;
while(i<s.size()){
if(j&&s[i]!=s[j])j=nxt[j];
else nxt[++i]=++j;
}
}
单次查找
bool kmp(string &s1,string &s2,int nxt[]){
int i=1,j=1;
while(i<s1.size()){
if(j&&s1[i]!=s2[j])j=nxt[j];
else i++,j++;
if(j==s2.size())return 1;
}
return 0;
}
全部位置
原理与border重心相同。
void kmp(string &s1,string &s2,int nxt[],vec<int> &ans){
int i=1,j=1;
while(i<s1.size()){
if(j&&s1[i]!=s2[j])j=nxt[j];
else i++,j++;
if(j==s2.size())ans.pub(i-j+1);
}
}
优化
不推荐使用。
这个优化在单次匹配时有效,但会导致求border和全部位置时失效,并且KMP本来就是 \(O(n)\) 算法,所以不推荐使用。
原理是如果下一个位置的字符与当前字符相同那显然不用匹配,跳过这一步即可。
以匹配重心为例。
int nxt[N];
void getnxt(string &s){
int i=1,j=0;
while(i<s.size()){
if(j&&s[i]!=s[j])j=nxt[j];
else{
if(s[++i]==s[++j])nxt[i]=nxt[j];
else nxt[i]=j;
}
}
}
例题
字符串匹配
题目中要求border,所以使用border重心会方便一些。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef __int128 i128;
typedef double db;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
template <typename Type>
using vec=vector<Type>;
template <typename Type>
using grheap=priority_queue<Type>;
template <typename Type>
using lrheap=priority_queue<Type,vector<Type>,greater<Type> >;
#define fir first
#define sec second
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
#define chmax(a,b) a=max(a,b)
#define chmin(a,b) a=min(a,b)
#define rep(i,x,y) for(int i=x;i<=y;i++)
#define per(i,x,y) for(int i=x;i>=y;i--)
const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int mod=998244353;
const int N=1e6+5;
string s1,s2;
int nxt[N];
void getnxt(string &s){
int i=1,j=0;
nxt[0]=-1;
while(i<s.size()){
if(j&&s[i]!=s[j])j=nxt[j-1]+1;
else nxt[i++]=j++;
}
}
void kmp(string &s1,string &s2,int nxt[],vec<int> &ans){
int i=1,j=1;
while(i<s1.size()){
if(j&&s1[i]!=s2[j])j=nxt[j-1]+1;
else i++,j++;
if(j==s2.size())ans.pub(i-j+1);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>s1>>s2;
s1=' '+s1;s2=' '+s2;
getnxt(s2);
vec<int> ans;
kmp(s1,s2,nxt,ans);
for(auto it:ans)cout<<it<<"\n";
rep(i,1,s2.size()-1)cout<<nxt[i]<<" ";
return 0;
}
当然也有匹配重心的写法。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef __int128 i128;
typedef double db;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
template <typename Type>
using vec=vector<Type>;
template <typename Type>
using grheap=priority_queue<Type>;
template <typename Type>
using lrheap=priority_queue<Type,vector<Type>,greater<Type> >;
#define fir first
#define sec second
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
#define chmax(a,b) a=max(a,b)
#define chmin(a,b) a=min(a,b)
#define rep(i,x,y) for(int i=x;i<=y;i++)
#define per(i,x,y) for(int i=x;i>=y;i--)
const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int mod=998244353;
const int N=1e6+5;
string s1,s2;
int nxt[N];
void getnxt(string &s){
int i=1,j=0;
while(i<s.size()){
if(j&&s[i]!=s[j])j=nxt[j];
else nxt[++i]=++j;
}
}
void kmp(string &s1,string &s2,int nxt[],vec<int> &ans){
int i=1,j=1;
while(i<s1.size()){
if(j&&s1[i]!=s2[j])j=nxt[j];
else i++,j++;
if(j==s2.size())ans.pub(i-j+1);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>s1>>s2;
s1=' '+s1;s2=' '+s2;
getnxt(s2);
vec<int> ans;
kmp(s1,s2,nxt,ans);
for(auto it:ans)cout<<it<<"\n";
rep(i,1,s2.size()-1)cout<<nxt[i+1]-1<<" ";
return 0;
}
nxt运用
有时候我们也会专门用到 nxt 数组。
另一种码风
全部以单次匹配为例吧,找出全部位置略加修改即可。
border重心
int nxt[N];
void getnxt(string &s){
nxt[0]=-1;
for(int i=1,j=0;i<s.size();i++,j++){
while(j&&s[i]!=s[j])j=nxt[j-1]+1;
nxt[i]=j;
}
}
bool kmp(string &s1,string &s2,int nxt[]){
for(int i=1,j=1;i<s1.size();i++,j++){
while(j&&s1[i]!=s2[j])j=nxt[j-1]+1;
if(j==s2.size()-1)return 1;
}
return 0;
}
匹配重心
int nxt[N];
void getnxt(string &s){
for(int i=1,j=0;i<s.size();){
while(j&&s[i]!=s[j])j=nxt[j];
nxt[++i]=++j;
}
}
bool kmp(string &s1,string &s2,int nxt[]){
for(int i=1,j=1;i<s1.size();i++,j++){
while(j&&s1[i]!=s2[j])j=nxt[j];
if(j==s2.size()-1)return 1;
}
return 0;
}
标签:nxt,typedef,string,int,算法,KMP,border,define
From: https://www.cnblogs.com/HarlemBlog/p/18620723