前缀函数与KMP算法
Posted mingyunyuansu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前缀函数与KMP算法相关的知识,希望对你有一定的参考价值。
title: 前缀函数与KMP算法
date: 2020-08-05
tags:
- 算法
- 字符串
- OI
categories:
- 技术
因为大二的时候全程划水,导致我对KMP只听说过名字。老师似乎都没展开讲,我记得是有一节下课时说这个算拓展内容,可以自己回去研究,所以我印象中还蛮难的。
前段时间在廖雪峰的网站重新学了一遍,自己代码实现了一下,感觉还蛮简单的,与印象中不符,有点奇怪。
直到今天在Leetcode上用KMP解一道字符串匹配居然超时了,才发现不对劲。
仔细查阅后才意识到,KMP的难点根本不在于算法本身,而在于构建前缀函数的过程。廖雪峰的网站把这个前缀函数是什么讲得很清晰了(也就是PMT,部分匹配表),但是如果按照朴素的想法来构建这个东西,会发现时间复杂度特别高:
//朴素方法,j即最长公共前后缀的长度
vector<int> pi(s.size());
for (int i = 1; i < s.size(); ++i) {
for (int j = i; j >= 0; --j) {
if (s.substr(0, j) == s.substr(i - j + 1, j))
pi[i] = j;
break;
}
}
双重循环内再加子串的比较,时间复杂度直接来到O(N^3)
。尽管构建后的表格能加速后面的比较,但是构建表格本身消耗立方时间的话会得不偿失,一定要优化这个方法。
看了半天,还是OI Wiki讲得靠谱。
优化
我们首先可以观察到一点就是,i + 1
位置的前缀函数pi[i + 1]
,最大也只能比pi[i]
大1。
这个应该不难理解。因为相对于上一个子串,长度只增加了一,就算是最完美的情况下,前缀函数(PMT)的值也只能加1。例如字符串abab
,前三个前缀函数是[0, 0, 1]
,现在看pi[3]
也就是第四位,前缀只能增加为2。
但是这里难点就在于,我们要思考是什么情况可以使得p[i + 1] == p[i] + 1
。好吧,我觉得一般人也思考不出来这个东西,答案就是s[i + 1] == s[pi[i]]
的时候。
这个式子初看直接懵逼,卡了我一小时,突然想到举个例子不就完了,于是举个例子,确实很简单。。。可惜我一个人自学,就是容易走进死胡同。
看例子saba
,其pi
为[0,0,1]
我们现在来填第四个字符,使得pi
变为[0,0,1,2]
。根据公式知道s[pi[2]] == ‘b‘
,变为abab
。
倒推下原因,因为pi[2] == 1
其实就是说ab(a)
,右边这个a
处的最长公共前后缀长度为1,其实就是前后的两个a
。现在想让pi[3] == p[2] + 1
,就是使得新的最长前缀变为ab
,那么其相应的后缀ax
,就只能是ab
,所以合并abax == abab
。
推广
上面的优化方式其实不仅能用边界情况的一次,其实可以一直往前推广。我们第一次是看abax
中的s[3]
和s[1]
的对比(也就是s[3]
和s[pi[2]]
),要是这个对比不相等,我们就继续比较s[3]
和s[pi[pi[2]] - 1]
。这么说好像更复杂了。。。直接看代码吧:
//j是当前最大前缀长度
vector<int> pi(s.size());
for (int i = 1; i < s. size(); ++i) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j]) j = pi[j - 1];//一次不成,就把结论往前推广
if (s[i] == s[j]) ++j;//成了,依然是+1
pi[i] = j;
}
循环隐藏
上面j
每次都是从最边界的情况开始往前迭代,其实还能偷懒,写成这样:
vector<int> pi(s.size());
for (int i = 1, j = 0; i < s.size() - 1;) //最后一位其实可以不算,因为没用
{
if (s[i] == s[j]) {
pi[i] = ++j;
++i;
}
else {
//把内层循环合并了
if (j == 0) {
++i;
}
else {
j = pi[j - 1];
}
}
}
复杂度分析
众所周知KMP时间复杂度是O(N+M)。查找的过程O(N)先不说,我现在还比较困惑为什么构建前缀函数的过程只有O(M)次操作。j不是在回滚吗?网上说用均摊分析,但是似乎都没说清楚,有知道的人能说一下吗?
以上是关于前缀函数与KMP算法的主要内容,如果未能解决你的问题,请参考以下文章