KMP算法

更新日志 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;
        }
    }
}

例题

字符串匹配

LG3375

题目中要求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 数组。

CF432D

提交记录

另一种码风

全部以单次匹配为例吧,找出全部位置略加修改即可。

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;
}
posted @ 2024-12-21 14:14  LastKismet  阅读(53)  评论(1编辑  收藏  举报