算法笔记(2):KMP 算法
介绍
KMP算法(全称Knuth-Morris-Pratt字符串查找算法,由三位发明者的姓氏命名)是可以在文本串 \(S\) 中 \(O(|S|+|P|)\) 查找模式串 \(P\) 的一种算法。
思想
考虑 \(O(|S||P|)\) 的暴力匹配算法,对于文本串 \(S\) 的每一位开始与模式串 \(P\) 逐一匹配
缺点在于每次都要重新开始逐一匹配,不能充分利用前面匹配得到的信息,导致算法效率低下
那么我们需要考虑的问题是 匹配失败后从哪一位开始重新匹配最好?
首先,我们需要引入一些概念
前缀函数
子串:\(S[i..j]\) \((i≤j)\),即 \(S[i],S[i+1],\ldots,S[j]\)
前缀:\(Prefix(S,k)=S[0..k]\)
真前缀:\(Prefix(S,k)=S[0..k]\) \((k \neq |S| - 1)\) 【不包含 \(S\) 本身】
后缀:\(Suffix(S,k)=S[k..|S|-1]\)
真后缀:\(Suffix(S,k)=S[k..|S|-1]\) \((k \neq 0)\) 【不包含 \(S\) 本身】
公共前后缀:\(Prefix(S,k) = Suffix(S,|S|-k-1)\)
真公共前后缀:\(Prefix(S,k) = Suffix(S,|S|-k-1)\) \((k \neq |S|-1)\) 【不包含 \(S\) 本身】
定义 前缀函数 \(\pi[i]\) : \(S[0..i]\) 的最长真公共前后缀的长度
即 \(\pi[i] = \max_{k = 0 \dots i-1}\{k: S[0..k] = S[i-k..i]\}\)
怎么维护?
-
暴力:枚举前缀子串 \(O(n)\),枚举该串的前后缀 \(O(n)\),比较 \(O(n)\),总 \(O(n^3)\)
-
注意到 \(S[0..i]\) 的真公共前后缀 移除最后一位后的串 是 \(S[0..i-1]\) 的真公共前后缀,我们可以根据这个构造递推方程
对于 \(k = 0 \dots i-2\),
如果 \(S[k+1] == S[i]\),\(\pi[i] = \max(\pi[i],\{k: S[0..k] = S[i-1-k..i-1]\} + 1)\)
否则 \(\pi[i] = \max(\pi[i],0)\)因此我们只需要从大到小枚举 \(S[0..i-1]\) 的所有真公共前后缀进行匹配就可以得出 \(S[0..i]\) 的最长真公共前后缀
那么如何从大到小枚举 \(S[0..i-1]\) 的所有真公共前后缀?
\(\overbrace{\underbrace{S_0S_1}_k S_2S_3}^{\pi[i-1]} \dots \overbrace{S_{i-4}S_{i-3} \underbrace{S_{i-2}S_{i-1}}_k}^{\pi[i-1]}S_i\)
假设下一个真公共前后缀 \(S[0..k] = S[i-1-k..i-1]\) \((k < \pi[i]-1)\) ①
因为 \(S[0..\pi[i-1]-1] = S[i-\pi[i-1]..i-1]\)
即 \(S[i-1-k..i-1] = S[\pi[i-1]-1-k..\pi[i-1]-1]\) ②
那么 \(S[0..k] = S[\pi[i-1]-1-k..\pi[i-1]-1]\) ①②如
假设 \(S_0S_1\) = \(S_{i-2}S_{i-1}\) ①
因为 \(S_0S_1S_2S_3\) = \(S_{i-4}S_{i-3}S_{i-2}S_{i-1}\)
即 \(S_{i-2}S_{i-1}\) = \(S_2S_3\) ②
那么 \(S_0S_1\) = \(S_2S_3\) ①②由于 \(S[0..k] = S[\pi[i-1]-1-k..\pi[i-1]-1]\) 且 这是下一个真公共前后缀
即 \(\pi[\pi[i-1]-1] = k\) 【\(\pi的定义\)】
那么找下标为 \(i-1\) 的下一个真公共前后缀 \(\Leftrightarrow\) 找下标为 \(\pi[i-1]-1\) 的最长公共前后缀综上,从大到小枚举 \(S[0..k]\) 的所有真公共前后缀只需要不断地将 \(k \to \pi[k]-1 \to \pi[\pi[k]-1]-1 \to .. -1\) 不断循环直到成功匹配或者枚举到-1即可
因此,求 \(S[i]\) ,令 \(k = i-1\),按照上面的枚举方式不断循环即可
如果成功匹配,那么 \(\pi[i] = \pi[k] + 1\)
否则 \(\pi[i] = 0\)代码
点击查看代码
vector<int> getSuffixFunction(string S){//0-based //"aabaaab" int n = S.size(); vector<int> pi(n); for(int i = 0; i < n; i ++ ){ //递推求pi int k = i-1; while(k != -1 && S[pi[k]] != S[i])k = pi[k]-1; //从大到小枚举S[0..k]的所有真公共前后缀 //k == -1:说明枚举过程结束 //S[pi[k]] == S[i]:说明匹配成功 if(k == -1 || S[pi[k]] != S[i])pi[i] = 0; //匹配失败 else pi[i] = pi[k] + 1; //匹配成功 } return pi; //[0,1,0,1,2,2,3] }
理解前缀函数的递推后,字符串匹配就简单了
将模式串 \(P\) 和 文本串 \(S\) 按照 \(P + ? + S\) 【?为 \(P\) 和 \(S\) 中均没有出现的字符】的形式拼接求前缀函数即可
模式串在文本串中的条件为 \(\pi[i] = |P|\),首位为 \(i - 2|P|\)
如
模式串 \(P\) = "ABA"
文本串 \(S\) = "ABABA"
新串:\(ABA?ABABA\)
下标:\(0\ 1\ 2\ 3\ 4\ 5\ 6\ 7\ 8\)
\(\pi\):$\ \ \ \ 0\ 0\ 1\ 0\ 1\ 2\ 3\ 2\ 3$
代码
点击查看代码
vector<int> KMP(string S,string P){//0-based
S = P + "?" + S;
int n = S.size();
vector<int> pi(n);
vector<int>res;
for(int i = 0; i < n; i ++ ){
int k = i-1;
while(k != -1 && S[pi[k]] != S[i])k = pi[k]-1;
if(k == -1 || S[pi[k]] != S[i])pi[i] = 0;
else pi[i] = pi[k] + 1;
if(pi[i] == P.size())res.push_back(i - 2*P.size());
//返回首位
}
return res;
}

浙公网安备 33010602011771号