目录
KMP简介:
在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个字符串S
内查找一个词W
的出现位置。一个词在不匹配时本身就包含足够的信息来确定下一个匹配可能的开始位置,此算法利用这一特性以避免重新检查先前配对的字符。——(维基百科)
用途:字符串匹配问题
提出问题:给你两个数字序列,你的任务时在序列 a 中找到和序列 b 完全匹配的子串,如果有多个匹配的位置,输出最小的那个。设文本串长度为 n,模式串长度为 m
暴力求解:
将模式串与文本串按位进行匹配,当不匹配时,将模式串右移一位,重新匹配,直到匹配成功或遍历完整个文本串。
时间复杂度:O(m*n)
KMP算法:
根据最长相等前后缀,构建 next 表,当不匹配时,按照 next 表进行跳转,重新匹配,直到匹配成功或遍历完整个文本串。
时间复杂度:O(m+n)
KMP正确性证明:
* 引理 32.5(前缀函数迭代引理)
给定长度为 m 的模式 P,其前缀函数 π。证明:对 q=1,2,…,m,
有[q]={k:k<q , Pk⊐Pq} 。
证:数学归纳证明 [q]⊆{k:k<q , Pk⊐Pq}
当 i=1时,[q]=[q]=π[q],
∵ 由定义 π[q]=max {k:k<q , Pk⊐Pq}
∴ π[q]∈{k:k<q , Pk⊐Pq}
假设,当 i=n−1时, [q]∈{k:k<q , Pk⊐Pq};
当 i=n 时,
∵ P[q]=Pπ[ [q]] ⊐P [q] ⊐Pq
∴ P[q]⊐Pq
∴ [q]∈{k:k<q , Pk⊐Pq}
∴ [q]⊆{k:k<q , Pk⊐Pq} 成立。
反正法证明 {k:k<q , Pk⊐Pq}⊆[q]
假设 {k:k<q , Pk⊐Pq}⊈[q],我们取非空集合 {k:k<q , Pk⊐Pq}−[q]中最大的元素 m,
取 n=min {k:k∈[q] , k>m}
由上面的证明,我们知道 n∈{k:k<q , Pk⊐Pq}
∴ Pn⊐Pq
∵ Pm⊐Pq , n>m
∴ Pm⊐Pn
∴ m≤π[n]
∵ m∉[q],π[n]∈[q]
∴ m≠π[n],m<π[n]
∵ π[n]<n
∴ m<π[n]<n,这与 n 和 m 的取法相矛盾(要么 m 不是非空集合 {k:k<q , Pk⊐Pq}−[q] 中最大的,要么 n 不是 {k:k∈[q] , k>m}中最小的)
∴ 假设不成立,即 {k:k<q , Pk⊐Pq}⊆[q]
综上所述,引理结论成立,证毕。
* 引理 32.6
给定长度为 m 的模式 P,其前缀函数 π。证明:对 q=1,2,…,m,如果 π[q]>0,
那么 π[q]−1∈[q−1]。
证: 令 r=π[q],那么 r<q, r−1<q−1,Pr⊐Pq
∵ r=π[q]>0
∴ Pr−1⊐Pq−1(通过把 Pr, Pq 的最后一个字符去掉)
∴ π[q]−1=r−1 ∈{k:k<q−1 , Pk⊐Pq−1}
由引理 32.5 可得 π[q]−1 ∈[q−1]
证毕。
* 定义
对 q=2,3,…,m,定义 [q−1] 的子集 Eq−1 如下:
Eq−1={ k∈[q−1]:P[k+1]=P[q]
={ k:k<q−1,Pk⊐Pq−1,P[k+1]=P[q] }(引理 32.5)
={ k:k<q−1,Pk+1⊐Pq }
* 推论 32.7
给定长度为 m 的模式 P,其前缀函数 π,那么对 q=2,3,…,m,
有 π[q]= 0 当 Eq−1=∅
1+max {k∈Eq−1} 当 Eq−1≠∅ 。
证: 当 Eq−1=∅ 时,由Eq−1={ k:k<q−1,Pk+1⊐Pq } 知,不存在 k<q−1,k+1<q ,使得 Pk+1⊐Pq
∴π[q]=0
当 Eq−1≠∅ 时,
∵ ∀ k ∈ Eq−1,有 Pk+1⊐Pq
∴ π[q]≥k+1≥max {k∈Eq−1}+1
即 π[q]≥max {k∈Eq−1}+1 ⋯ (1)
另一方面,由上我们知道 π[q]>0,令 r=π[q]−1,由引理 32.6,得 r∈[q−1] ⋯ (2)
∵ r+1=π[q]
∴ Pr+1⊐Pq ⋯ (3)
由 (2),(3) 两式,我们得到 r∈Eq−1
∴ r≤max {k∈Eq−1}
∴ π[q]−1≤max {k∈Eq−1}
即 π[q]≤max {k∈Eq−1}+1 ⋯ (4)
由 (1),(4) 两式,可得 π[q]=max {k∈Eq−1}+1
综上所述,推论得证。
举例说明:
文本串:aabaabaaf
模式串:aabaaf
分析:将模式串与文本串按位进行匹配,发现 f 与 b 不匹配,KMP算法思路为求取 f 前的字串 aabaa 的最长相等前后缀为 aa,此时跳转到 aa 的下一次字符 b 进行重新匹配。
名词解释:
前缀:除最后一个字符以外,字符串的所有头部字串。
后缀:除第一个字符以外,字符串的所有尾部字串。
最长相等前后缀:找出 P 的所有前缀集合 CP,找出 P 的所有后缀集合 CS,求 CP 和 CS 的交集中长度最大的元素,称之为 P 的最长相等前后缀。
引入 next 表:
1.求最长相等前后缀
模式串 | 前缀 | 后缀 | 最长相等前后缀 | 长度 |
a | 无 | 无 | 无 | 0 |
aa | a | a | a | 1 |
aab | a,aa | b,ab | 无 | 0 |
aaba | a,aa,aab | a,ba,aba | a | 1 |
aabaa | a,aa,aab,aaba | a,aa,baa,abaa | aa | 2 |
aabaaf | a,aa,aab,aaba,aabaa | f,af,aaf,baaf,abaaf | 无 | 0 |
2.构建模式串的 next 表
文本串 | a | a | b | a | a | f |
Index | 0 | 1 | 2 | 3 | 4 | 5 |
Next | 0 | 1 | 0 | 1 | 2 | 0 |
next 表说明:
以上述为例,当 f 不匹配时,查取 f 的前一个字符 a 的 Next值为 2,由此跳转至 Index 为 2 的字符 b,此时将从 b 进行重新匹配,直到匹配成功或遍历完整个文本串。
文本串 | a | a | b | a | a | b | a | a | f |
模式串 | a | a | b | a | a | f |
文本串 | a | a | b | a | a | b | a | a | f |
模式串 | a | a | b | a | a | f |
文本串 | a | a | b | a | a | b | a | a | f |
模式串 | a | a | b | a | a | f |
文本串 | a | a | b | a | a | b | a | a | f |
模式串 | a | a | b | a | a | f |
代码举例:
说明:将 next[0] 设置为 -1,即如果第一个字符就不匹配,则将模式串右移一位后继续匹配。
问题再现:给你两个数字序列,你的任务时在序列 a 中找到和序列 b 完全匹配的子串,如果有多个匹配的位置,输出最小的那个。
1.构造 next 表
void GetNextTab(int m){
int j = 0;
nextTab[j] = -1;
int i = nextTab[j];
while(j < m){
if(i == -1 || pattern[i] == pattern[j]){
i++;
j++;
nextTab[j] = i;
}
else i = nextTab[i];
}
return;
}
2.KMP算法
int KMP(int n, int m){
GetNextTab(m);
int i = 0;
int j = 0;
while(i < n && j < m){
if(j == -1 || text[i] == pattern[j]){ //当前字符串匹配成功
i++;
j++;
}
else j = nextTab[j]; //当前字符串匹配失败
}
if(j == m) return i - j + 1; //模式串匹配成功
else return -1; //模式串匹配失败
}
完整代码
#include<iostream>
#include<cstdio>
using namespace std;
int nextTab[1000];
int text[1000];
int pattern[1000];
void GetNextTab(int m){
int j = 0;
nextTab[j] = -1;
int i = nextTab[j];
while(j < m){
if(i == -1 || pattern[i] == pattern[j]){
i++;
j++;
nextTab[j] = i;
}
else i = nextTab[i];
}
return;
}
int KMP(int n, int m){
GetNextTab(m);
int i = 0;
int j = 0;
while(i < n && j < m){
if(j == -1 || text[i] == pattern[j]){ //当前字符串匹配成功
i++;
j++;
}
else j = nextTab[j]; //当前字符串匹配失败
}
if(j == m) return i - j + 1; //模式串匹配成功
else return -1; //模式串匹配失败
}
int main(){
int casenum;
scanf("%d", &casenum);
while(casenum--){
int n, m;
scanf("%d%d", &n, &m);
for(int i=0; i<n; ++i) scanf("%d", &text[i]);
for(int i=0; i<m; ++i) scanf("%d", &pattern[i]);
printf("%d\n", KMP(n, m));
}
return 0;
}