KMP 学习笔记
把之前留存在 dp 学习记录里的扒下来了,也方便寻找。
发现 KMP 重要的实际不在 KMP 本体,而是前缀函数的处理,知道怎么处理前缀函数就知道怎么写 KMP 了。
前缀函数在 OIWIKI 上有详解,因为个人看第二个优化感觉有点糊,所以重点写一下第二个优化。
令前缀函数为 \(\pi_i\)。
优化一:
注意到对于所有的 \(i\),\(\pi_i\) 最大也只等于 \(\pi_{i-1}+1\)。
所以可以限定上界,优化枚举次数。
优化二:
我们可以发现,实际上有效的转移只会是 \(\pi_{i-1}\) 的不断迭代,即类似于这种形式 \(\pi(\pi(\pi(i-1)-1)-1)\),直到迭代到零。
采用反证法。
我们有字符串 \(s\)。
\(s_0,s_1,s_2,s_3,......,s_{i-3},s_{i-2},s_{i-1},s_{i},s_{i+1}\)。
令 \(s_{i,j}\) 表示从 \(s\) 中的第 \(i\) 个字符到第 \(j\) 个字符。
以上字符串满足 \(\pi_i=4,\pi_3=2\)。
即 \(s_{0,3}=s_{i-3,i},s_{0,1}=s_{2,3}\)。
由上可以推得:
\(s_{0,1}=s_{i-1,i}\)
假设 \(\pi_{i+1}=4\)。
$\Downarrow $
\(s_{0,3}=s_{i-2,i+1}\)
$\Downarrow $
\(s_{0,2}=s_{i-2,i}\)
$\Downarrow $
\(s_{1,3}=s_{i-2,i}\)
则 \(\pi_{3}=3\),与题目条件不符,故假设不成立。
所以 \(\pi_{i+1}\ne 4\)。
因为更短的转移,会更劣,所以不考虑。(都更短了还能不更劣吗)
有点粗糙,但是应该能看懂,以后每层迭代都可以套用以上证明。
至于字符串匹配,考虑使用一个分隔符(既不在字符串中也不在文本中)将字符串与文本连接起来,然后直接求前缀函数,看哪一位的前缀函数等与字符串长度就代表匹配到了。这部分还是挺简单易懂的,难搞的还是前缀函数。
实现代码如下:
vector<int> pre_func(string s)
{
int n = s.size(), x;
vector<int> pre(n);
for (int i = 1; i < n; i++)
{
x = pre[i - 1];
while (x && s[i] != s[x])
x = pre[x - 1];
if (s[i] == s[x])
++x;
pre[i] = x;
}
return pre;
}
void find_func(string text, string s)
{
string s0 = s + '#' + text;
vector<int> pre = pre_func(s0);
int n = s.size();
for (int i = n + 1; i < s0.size(); i++)
if (pre[i] == n)
cout << i - (n << 1) + 1 << "\n";
for (int i = 0; i < n; i++)
cout << pre[i] << " ";
}
可以根据需求改变文本和字符串的类型。