学习NOTE 6——KMP
KMP Loading. . . . . .
是什么
KMP 算法是一种改进了的字符串匹配算法,用来快速找出一个短字符串(模式串)在另一个长字符串(文本串)中出现的所有位置。这个算法是由$ \textbf{D.E.Knuth,J.H.Morris 和 V.R.Pratt} $这三个大神犇发明的。
为什么叫“改进了的”? 因为KMP可以用\(O(n + m)\)的时间复杂度完成匹配,远胜于暴力查找的\(O(n \times m)\)。牛逼!!(其中\(n\)和\(m\)指文本串长度和模式串长度,下同)
字符串匹配
我们之前学过的暴力是这样的:
for(int i=0;i+t.size()-1<s.size();i++){
bool flg=1;
for(int j=0;j<t.size();j++){
if(s[i+j-1]!=t[j]){
flg=0;
break;
}
}
if(flg) cout<<i<<" "<<i+t.size()-1<<"\n";
}
时间复杂度为\(O(n \times m)\)(最坏情况)。
举个栗子:
假设:
- 文本串
s = "AAAAAB" - 模式串
t = "AAAAB"
暴力匹配的做法:
- 从s的第1位开始比较:
- \(A=A ✓,A=A ✓,A=A ✓,A=A ✓,\color{Red}B \ne A ✗\)(第5位失败)
- 然后回到s的第2位,重新从t的第1位开始比较...
- 这样一直试到第2位才成功。
问题:每次匹配失败,文本串指针都要回退,导致很多字符被重复比较。最坏情况下时间复杂度是 \(O(n \times m)\)(n是文本长度,m是模式长度),当n和m都很大时(比如\(10^6\)),会超时。
KMP代码实现
匹配过程中已经比较过的文本字符,不要浪费! 即使某次匹配失败了,我们也知道"已经匹配了多长"和"这些匹配上的字符是什么",可以利用这个信息让模式串跳着走,而不是从头开始。
这时,我们需要记录一个\(p\)数组,用来表示子串 \(t_{0..i}\) 中,最长的相等前缀和后缀的长度(且长度小于 \(i+1\))。
也就是说:当匹配到模式串的 \(t_i\) 时失败了,说明前面的 \(t_{0..i-1}\) 都匹配成功了。那我们可以把模式串往后移动一段距离,移动多少呢?移动 \(i - p_{i-1}\) 位,也就是让模式串的 \(p_{i-1}\) 位置对准当前文本位置继续比较。
栗子
- 文本串
s = "ABABABC" - 模式串
t = "ABABC"
第一步:先算模式串的前缀函数
t = "ABABC"
p[0]=0(单字符)p[1]=0("AB",前缀"A"≠后缀"B")p[2]=1("ABA",前缀"A"=后缀"A")p[3]=2("ABAB",前缀"AB"=后缀"AB")p[4]=0("ABABC",没有相等的前后缀)
所以 p = [0, 0, 1, 2, 0]
第二步:匹配(重点!)
s: A B A B A B C
t: A B A B C
- i=0, j=0:\(A=A\) ✓,i=1, j=1
- i=1, j=1:\(B=B\) ✓,i=2, j=2
- i=2, j=2:\(A=A\) ✓,i=3, j=3
- i=3, j=3:\(B=B\) ✓,i=4, j=4
- i=4, j=4:$A \ne C $✗(失配!)
暴力做法:回退到s的第2位(B),t从头(A)开始。
KMP做法:
- 当前j=4(匹配了4个字符,
"ABAB") - 查表:
p[3] = 2(因为失配在t[4],我们看t[0..3]的最长相等前缀和后缀长度) - 让
j = p[3] = 2,i不变(i=4) - 现在比较
s[4]='A'与t[2]='A'✓
- i=4, j=2:\(A=A\) ✓,i=5, j=3
- i=5, j=3:\(B=B\) ✓,i=6, j=4
- i=6, j=4:\(C=C\) ✓,匹配成功!位置 = 6-5+1 = 2(从1开始计数)
See? 文本串指针i从来没有回退,只是模式串的指针\(j\)跳到了\(2\),省去了重复比较"AB"的时间。
代码
PID:P3375
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
int p[N];//存储前缀函数
string s,t;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>s>>t;
int n=s.size(),m=t.size();
for(int i=1,j=0;i<m;i++){//前缀函数
while(j>0&&t[i]!=t[j]) j=p[j-1];//失配时回退j
if(t[i]==t[j]) j++;//匹配则前进
p[i]=j;
}
for(int i=0,j=0;i<n;){//j:字符串t的下标 i:字符串s的下标
if(s[i]==t[j]) i++,j++;
else if(j>0) j=p[j-1];//如果字符串不匹配重新计算
else i++;//继续往后延伸字符串s
if(j==m){//如果成功匹配字符串
j=p[j-1];//回退模式串指针,目的是为了寻找下一个
cout<<i-m+1<<"\n";//输出匹配起始位置
}
}
for(int i=0;i<m;i++) cout<<p[i]<<" ";
return 0;
}

浙公网安备 33010602011771号