前缀函数与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算法的主要内容,如果未能解决你的问题,请参考以下文章

扩展KMP算法

前缀数组

对KMP算法小理解

KMP算法的正确性证明及一个小优化

KMP算法

KMP算法