菜缤的世界 CairBin's Blog - KMP
type: channel
link: https://blog.cairbin.top/index.php/tag/KMP/
items: KMP算法与前缀函数

KMP算法与前缀函数
type: item
date: 2024-02-10T14:53:00+00:00
description: KMP算法与前缀函数说明本文参考了 OI Wiki。首先来明确几个概念：后缀：后缀是从父串某个位置i开始到末尾结束的一个特殊字符串。真后缀：不为父串本身的后缀字符串。前缀：从父串开头到某个位置i结束的一个特殊字符串。真前缀：不为父串的前缀字符串。前缀函数给定一个长度为$n$的字符串$s$，其前缀函数被定义一个长度为$n$的数组$\pi$，其定义为：如果子串$s[0...i]$有一对相等的真前缀与真后缀：$s[0...k-1]$和$s[i-(k-1)...i]$，那么$\pi [i]$就是这个相等的真前缀（或者真后缀，因为它们相等)的长度，也就是$\pi [i] = k$如果不止有一对相等的，那么$\pi [i]$就是其中最长的那一对的长度；如果没有相等的，那么$\pi [i] = 0$。简单来讲$\pi [i]$就是字串$s[0...i]$最长相等的真前缀和真后缀的长度，且特别规定$\pi [0] = 0$。我们由此定义给出代码：//前缀函数 vector<int> prefix; //前缀函数对应的数组，当然你可以将它写在prefix_func里面并最后返回 void prefix_func(const string& s) { int n = (int)s.length(); prefix.resize(n); for (int i = 1; i < n; i++) { int j = prefix[i - 1]; while (j > 0 && s[i] != s[j]) j = prefix[j - 1]; if (s[i] == s[j]) j++; prefix[i] = j; } }KMP算法复杂度时间复杂度为 $O(n+m)$空间复杂度为 $O(n)$原理Knuth–Morris–Pratt 算法原理如下：给定一个文本$t$和一个字符串$s$，我们尝试找到$s$ 在$t$中的所有出现（用第一个字符的下标表示）。为了简便起见，我们用$n$表示字符串$s$的长度，用$m$表示文本$t$的长度。我们构造一个字符串 $s + \# + t$，其中$\#$为一个既不出现在$s$中也不出现在$t$中的分隔符。接下来计算该字符串的前缀函数。现在考虑该前缀函数除去最开始个$n+1$值（即属于字符串$s$和分隔符的函数值）后其余函数值的意义。根据定义，$\pi [i]$为右端点在$i$且同时为一个前缀的最长真子串的长度，具体到我们的这种情况下，其值为与$s$的前缀相同且右端点位于$i$的最长子串的长度。由于分隔符的存在，该长度不可能超过$n$。而如果等式$\pi [i] = n$成立，则意味着$s$完整出现在该位置（即其右端点位于位置$i$）。注意该位置的下标是对字符串$s + \# + t$而言的。因此如果在某一位置$i$有$\pi [i] = n$成立，则字符串$t$在字符串$i - (n-1) - (n+1)$的$i - 2n$处出现。正如在前缀函数的计算中已经提到的那样，如果我们知道前缀函数的值永远不超过一特定值，那么我们不需要存储整个字符串以及整个前缀函数，而只需要二者开头的一部分。在我们这种情况下这意味着只需要存储字符串$s + \#$以及相应的前缀函数值即可。我们可以一次读入字符串$t$的一个字符并计算当前位置的前缀函数值。// kmp算法，haystack为文本，needle为所查找的字符串 vector<int> kmp(string haystack, string needle) { string cur = needle + "#" + haystack; int sz1 = haystack.size(); int sz2 = needle.size(); vector<int>v; prefix_func(cur); for (int i = sz2 + 1; i <= sz1 + sz2; i++) { if (prefix[i] == sz2) v.push_back(i - 2 * sz2); //如果要首次出现的位置，从这里返回即可 } return v; }题目LeetCode 第 28 题 找出字符串中第一个匹配项的下标