NOIP 备赛指南 (2):字符串匹配(KMP)
KMP 算法
一、基础知识
1. 字符串匹配
给出两个字符串 s 1 s_1 s1 和 s 2 s_2 s2,若 s 1 s_1 s1 的区间 [ l , r ] [l,r] [l,r] 子串与 s 2 s_2 s2 完全相同,则称 s 2 s_2 s2 在 s 1 s_1 s1 中出现了,其出现位置为 l l l。现在请你求出 s 2 s_2 s2 在 s 1 s_1 s1 中所有出现的位置。保证 1 ≤ ∣ s 1 ∣ , ∣ s 2 ∣ ≤ 10 6 1\le|s1|,|s2|\le10^6 1≤∣s1∣,∣s2∣≤106。
上述是一个经典的字符串匹配( 1 1 1 对 1 1 1)问题。
(1) 简单暴力
时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 枚举序列匹配时的出现位置 l l l
- 检查此时匹配的每一个位置是否相同,如果全部相同则记录答案
(2) 字符串哈希
时间复杂度: O ( n + m ) O(n+m) O(n+m)
- 把字符串看作一个
BASE进制的大数,将其映射为一个整数 - 预先算出文本串所有前缀的哈希值,这样就能 O ( 1 ) O(1) O(1) 地算出任意子串的哈希值
- 算出 s 2 s_2 s2 的哈希值,再枚举 s 1 s_1 s1 中每个长度为 ∣ s 2 ∣ |s_2| ∣s2∣ 的子串的哈希值,看两者数字是否相等
(3) KMP
- 预处理模式串生成
next[] - 利用已匹配的信息避免回溯
如你所见, KMP \text{KMP} KMP 的核心在于前缀函数的预处理,通过避免冗余匹配提升效率。因此,我们成功将时间复杂度优化至线性 O ( n + m ) O(n + m) O(n+m)。
2. 前置概念
(1) 前缀
即 p r e ( s , k ) \mathrm{pre}(s,k) pre(s,k),是从字符串 s s s 中取前 k k k 个字符构成的字符串
(2) 后缀
即 s u f ( s , k ) \mathrm{suf}(s,k) suf(s,k),是从字符串 s s s 中取后 k k k 个字符构成的字符串,当 k = 0 k = 0 k=0 时为空串。
(3) Border
若整数 r r r 满足 0 < r < ∣ s ∣ 0 < r < |s| 0<r<∣s∣ 且 p r e ( s , r ) = s u f ( s , r ) \mathrm{pre}(s,r) = \mathrm{suf}(s,r) pre(s,r)=suf(s,r),则称 p r e ( s , r ) \mathrm{pre}(s,r) pre(s,r) 为 s s s 的一个 Border \text{Border} Border。
一个字符串可能有多个 Border \text{Border} Border,例如对于 s = a b c a b c a b s = \mathrm{abcabcab} s=abcabcab, a b \mathrm{ab} ab 和 a b c a b \mathrm{abcab} abcab 是 s s s 的 Border \text{Border} Border。记 l p s ( s ) \mathrm{lps}(s) lps(s) 表示字符串 s s s 的最长 Border \text{Border} Border。
3. P3375 【模板】KMP
模版背诵熟练即可,两个函数做到能完整默写,具体原理稍微有点复杂。
#include<bits/stdc++.h>
using namespace std;
const int MAXL=1e6+8;
string s1,s2;
int lps[MAXL],ocnt,occ[MAXL];//lps[i]:s2[1,i]的最长相等前后缀长度
void get_lps(const string&s){
int len=s.size()-1;
for(int i=2,j=0;i<=len;i++){//s1[...,i-1]与s2[1,j]匹配
while(j>0&&s[i]!=s[j+1])j=lps[j];
if(s[i]==s[j+1])j++;
lps[i]=j;
}
}
void kmp(const string&s,const string&t){
ocnt=0;
int slen=s.size()-1,tlen=t.size()-1;
if(tlen==0)return;
get_lps(t);
for(int i=1,j=0;i<=slen;i++){//s1[...,i-1]与s2[1,j]匹配
while(j>0&&s[i]!=t[j+1])j=lps[j];
if(s[i]==t[j+1])j++;
if(j==tlen)occ[++ocnt]=i-j+1,j=lps[j];
}
}
int main(){
cin>>s1>>s2,s1="#"+s1,s2="#"+s2;
kmp(s1,s2);
for(int i=1;i<=ocnt;i++)cout<<occ[i]<<"\n";
for(int i=1;i<s2.size();i++)cout<<lps[i]<<" ";
return 0;
}
二、延伸知识
1. exKMP
(1) 定义
扩展 KMP \text{KMP} KMP 算法( Extended KMP \text{Extended KMP} Extended KMP),通常也称为 Z Z Z 函数,它解决的问题是:给定一个文本串 S S S 和模式串 P P P,求 P P P 与 S S S 的每一个后缀的最长公共前缀( LCP \text{LCP} LCP)的长度。
其中, Z 1 Z_1 Z1 通常定义为 0 0 0 或字符串总长 n n n。
示例:
- aaaaa z ( aaaaa ) = [ 5 , 4 , 3 , 2 , 1 ] \texttt{aaaaa} \quad z(\texttt{aaaaa}) = [5, 4, 3, 2, 1] aaaaaz(aaaaa)=[5,4,3,2,1]
- aaabaab z ( aaabaab ) = [ 7 , 2 , 1 , 0 , 2 , 1 , 0 ] \texttt{aaabaab} \quad z(\texttt{aaabaab}) = [7, 2, 1, 0, 2, 1, 0] aaabaabz(aaabaab)=[7,2,1,0,2,1,0]
- abacaba z ( abacaba ) = [ 7 , 0 , 1 , 0 , 3 , 0 , 1 ] \texttt{abacaba} \quad z(\texttt{abacaba}) = [7, 0, 1, 0, 3, 0, 1] abacabaz(abacaba)=[7,0,1,0,3,0,1]
扩展 KMP \text{KMP} KMP 算法通常需要计算两个数组:
-
Z Z Z 数组(仅单个字符串自身)
对于字符串 P P P, Z [ i ] Z[i] Z[i] 表示子串 P [ i … n ] P[i\dots n] P[i…n](从第 i i i 位开始的后缀)与整串 P [ 1 … n ] P[1\dots n] P[1…n] 的最长公共前缀长度。 -
Ext \text{Ext} Ext 扩展数组(文本串 S S S + + + 模式串 P P P)
Ext [ i ] \text{Ext}[i] Ext[i] 表示:模式串 P P P 与文本串从第 i i i 位开头的后缀 S [ i … m ] S[i\dots m] S[i…m]的最长公共前缀长度。
核心:维护已知信息的最右区间 [ l , r ] [l,r] [l,r]。
(2) 模版
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXL=2e7+8;
string s1,s2;
int lcp[MAXL],ext[MAXL];
void get_lcp(const string&s){
int len=s.size()-1,l=1,r=0;
lcp[1]=len;
for(int i=2;i<=len;i++){
if(i<=r)lcp[i]=min(lcp[i-l+1],r-i+1);
while(i+lcp[i]<=len&&s[i+lcp[i]]==s[1+lcp[i]])lcp[i]++;
if(i+lcp[i]-1>r)l=i,r=i+lcp[i]-1;
}
}
void exkmp(const string&s,const string&t){
int slen=s.size()-1,tlen=t.size()-1,l=1,r=0;
if(tlen==0)return;
get_lcp(t);
while(r+1<=slen&&r+1<=tlen&&s[r+1]==t[r+1])r++;
ext[1]=r;
for(int i=2;i<=slen;i++){
if(i<=r)ext[i]=min(lcp[i-l+1],r-i+1);
while(i+ext[i]<=slen&&1+ext[i]<=tlen&&s[i+ext[i]]==t[1+ext[i]])ext[i]++;
if(i+ext[i]-1>r)l=i,r=i+ext[i]-1;
}
}
int main(){
cin>>s1>>s2,s1="#"+s1,s2="#"+s2;
exkmp(s1,s2);
ll ans1=0,ans2=0;
for(int i=1;i<s2.size();i++)ans1^=1ll*i*(lcp[i]+1);
for(int i=1;i<s1.size();i++)ans2^=1ll*i*(ext[i]+1);
cout<<ans1<<"\n"<<ans2;
return 0;
}
2. 马拉车算法
(1) 定义
由于作者也没有搞懂,所以待会回来搞一搞。
(2) 模版
#include<bits/stdc++.h>
using namespace std;
const int MAX2L=2e6+8;
string s;
int rad[MAX2L];
void manacher(const string&s){
string t="$#";
for(char ch:s)t+=ch,t+='#';
int len=t.size()-1,l=1,r=0;
for(int i=1;i<=len;i++){
if(i<=r)rad[i]=min(rad[l+r-i],r-i+1);
while(i-rad[i]>=1&&i+rad[i]<=len&&t[i-rad[i]]==t[i+rad[i]])rad[i]++;
if(i+rad[i]-1>r)l=i-rad[i]+1,r=i+rad[i]-1;
}
}
int main(){
cin>>s;
manacher(s);
int ans=0,len=s.size()*2-1;
for(int i=1;i<=len;i++)ans=max(ans,rad[i]-1);
cout<<ans;
return 0;
}
三、经典例题
1. P4391 [BalticOI 2009] Radio Transmission 无线传输
【[BalticOI 2009] Radio Transmission 无线传输】
易证: ans = L − lps L \text{ans}=L-\text{lps}_L ans=L−lpsL(可视作 lps \text{lps} lps 的一个性质)。
例如: s = s= s= cabcabca,则 lps L = 5 \text{lps}_L=5 lpsL=5(cabca),循环节为 L − lps L = 8 − 5 = 3 L-\text{lps}_L=8-5=3 L−lpsL=8−5=3。
#include<bits/stdc++.h>
using namespace std;
const int MAXL=1e6+8;
int n;
string s;
int lps[MAXL],ocnt,occ[MAXL];//lps[i]:s.substr(1,i)的最长相等前后缀长度
void get_lps(const string&s){
int len=s.size()-1;
for(int i=2,j=0;i<=len;i++){//s[...,i-1]与s[1,j]匹配
while(j>0&&s[i]!=s[j+1])j=lps[j];
if(s[i]==s[j+1])j++;
lps[i]=j;
}
}
int main(){
cin>>n>>s,s="#"+s;
get_lps(s);
cout<<n-lps[n];
return 0;
}
2. 其他常见问题
(1) 最短周期
- 最优( KMP \text{KMP} KMP): a n s = ∣ s ∣ − lps [ ∣ s ∣ ] ans=|s|-\text{lps}[|s|] ans=∣s∣−lps[∣s∣]
- Z \text{Z} Z 函数:计算 s s s 的 Z \text{Z} Z 函数,找到最小的 i ∣ ( ∣ s ∣ ) i\mid(|s|) i∣(∣s∣),使得 i + z i + 1 = ∣ s ∣ i+z_{i+1}=|s| i+zi+1=∣s∣
(2) 所有 border
- 最优( KMP \text{KMP} KMP):令 r = lps [ ∣ s ∣ ] r=\text{lps}[|s|] r=lps[∣s∣],循环执行 r = lps [ r ] r=\text{lps}[r] r=lps[r],每次得到的 r r r 为合法 border \text{border} border 长度
- Z \text{Z} Z 函数:计算 s s s 的 Z \text{Z} Z 函数, ∀ i ∈ { 1 , 2 , … , ∣ s ∣ } ∧ i + z i − 1 = ∣ s ∣ \forall i\in\{1,2,\dots,|s|\}\land i+z_i-1=|s| ∀i∈{1,2,…,∣s∣}∧i+zi−1=∣s∣, s [ i , i + z i − 1 ] s[i,i+z_i-1] s[i,i+zi−1] 就是一个答案
(3) t 在 s 中的所有出现位置
- 最优( KMP \text{KMP} KMP):预处理 t t t 的 lps \text{lps} lps 数组,双指针遍历 s s s 匹配,完整匹配时记录起点,令 j = lps [ j ] j=\text{lps}[j] j=lps[j] 继续匹配
- Z \text{Z} Z 函数:构造 t + # + s t+\#+s t+#+s,计算其 Z \text{Z} Z 函数, ∀ i ∈ { ∣ t ∣ + 2 , ∣ t ∣ + 3 , … , ∣ t ∣ + 1 + ∣ s ∣ } ∧ z i = ∣ t ∣ \forall i\in\{|t|+2,|t|+3,\dots,|t|+1+|s|\}\land z_i=|t| ∀i∈{∣t∣+2,∣t∣+3,…,∣t∣+1+∣s∣}∧zi=∣t∣, i − ∣ t ∣ − 1 i-|t|-1 i−∣t∣−1$就是一个答案的起始位置
(4) 最长回文前缀
- 最优( KMP \text{KMP} KMP):构造翻转串 s ′ + # + s s'+\#+s s′+#+s,求拼接串 lps \text{lps} lps 数组, lps [ len ( s ′ + # + s ) ] \text{lps}[\text{len}(s'+\#+s)] lps[len(s′+#+s)] 即为答案
- Z \text{Z} Z 函数:构造 s + # + s ′ s+\#+s' s+#+s′,其中 s ′ s' s′ 表示 s s s 反转后的字符串,计算其 Z \text{Z} Z 函数,然后从 # \# # 后找到第一个 i i i 使得 i + z i − 1 = 2 ∣ s ∣ + 1 i+z_i-1=2|s|+1 i+zi−1=2∣s∣+1, z i z_i zi 就是答案
(5) s 的每个后缀与 t 的 LCP
- 最优(扩展 KMP \text{KMP} KMP):直接计算 Ext \text{Ext} Ext 数组, Ext [ i ] \text{Ext}[i] Ext[i] 为 s s s 第 i i i 个后缀与 t t t 的 LCP \text{LCP} LCP
- Z \text{Z} Z 函数:构造 t + # + s t+\#+s t+#+s,计算其 Z \text{Z} Z 函数, z ∣ t ∣ + 2 , z ∣ t ∣ + 3 , … , z ∣ t ∣ + 1 + ∣ s ∣ z_{|t|+2},z_{|t|+3},\dots,z_{|t|+1+|s|} z∣t∣+2,z∣t∣+3,…,z∣t∣+1+∣s∣ 即为答案
(6) 所有前缀出现次数
两种方法效率持平
-
KMP \text{KMP} KMP 写法:初始化 c n t [ i ] = 1 cnt[i]=1 cnt[i]=1,倒序遍历 i = ∣ s ∣ → 2 i=|s|\to2 i=∣s∣→2,执行 c n t [ lps [ i ] ] + = c n t [ i ] cnt[\text{lps}[i]]+=cnt[i] cnt[lps[i]]+=cnt[i]
-
Z \text{Z} Z 函数:计算 s s s 的 Z \text{Z} Z 函数, z i z_i zi 就相当于让长度为 1 ∼ z i 1\sim z_i 1∼zi 的前缀出现次数加 1 1 1,差分即可
更多推荐


所有评论(0)