KMP 入门

再次学习 \(\rm KMP\) 后不一样的理解。

一些概念

  1. 定义字符串 \(S\) 的真 前/后 缀为非自身的 前/后 缀。

  2. 定义字符串 \(S\)\(border\)\(S\) 的公共真 前/后 缀。

  3. 定义字符串 \(S\) 的最长 \(border\)\(\pi\),对于 \(S\) 的每个前缀 \(S_{1 \sim i}\)\(\pi_i\) 为其最长 \(border\)\(\pi\) 函数就是所谓的前缀函数。

前缀函数的性质

  1. 相邻前缀函数 \(\pi_{i + 1} \le \pi_i + 1\)

使用反证法易证。

  1. 字符串 \(S\) 的所有 \(border\) 可以由 \(\pi_{|S|}\) 开始不断跳 \(\pi\) 由长度从大到小遍历。

只需证明 \(S_{1 \sim \pi_{\pi_n}}\)\(S\) 的次长 \(border\) 即可。

通过反证法也易证上述转化结论。

  1. 相邻前缀函数满足 \(\pi_{i + 1} = \max\limits_{T ~ is ~ the ~ border ~ of ~ S_{1 \sim i}, S_{i + 1} = S_{|T| + 1}} |T| + 1\)

使用反证法易证。

前缀函数的线性求法

有了上述的几条性质,我们直接利用性质 \(2, 3\) 即可得到一个求解前缀函数的做法:

  1. 首先利用性质 \(3\) 的结论,大体思路上上我们通过递推从 \(\pi_{i - 1} \rightarrow \pi_i\)

  2. 再利用性质 \(2\),不断地在 \(i\) 处从长到短跳 \(border\) 直到第一个满足 \(S_{i + 1} = S_{|T| + 1}\)\(border ~ T\) 就停止,令 \(\pi_i = |T| + 1\)

直观感受上这个做法是 \(\mathcal{O(n ^ 2)}\) 的,但实际上它是线性的,复杂度证明如下:

不难发现复杂度来源在于跳 \(\pi\),下面我们将证明跳 \(\pi\) 的总次数是线性的。

注意 \(i - \pi_i\) 的位置变化情况,不难发现其总是不会向前偏移的,而停留的次数最多不超过 \(n\) 次,总共位移长度也不超过 \(n\),显然跳 \(\pi\) 的次数是不超过上述两者之和 \(n + n = 2n\) 的。

前缀函数的基本应用

KMP

求解模式串 \(T\) 在匹配串 \(S\) 中的出现位置的问题。

市面上常见的 \(\rm KMP\) 算法是再利用 \(\pi\) 函数的性质去减少匹配次数,仔细观察其过程会发现本质上和求 \(\pi\) 函数的过程非常类似,这里我们直接将其过程划归到求 \(\pi\) 函数的问题上去。

构造一个新的字符串 \(T \# S\) 其中 \(\#\) 是一个既没有在 \(T\) 中出现也没有在 \(S\) 中出现的分隔符字符。

对这个新的字符串求其 \(\pi\) 函数,不难发现由于分隔符的出现,\(\forall i, \pi_i \le |T|\),于是找到 \(\pi_i = |T|\) 的位置,\(i - |T| + 1 \sim i\) 就是一个匹配。

统计每个前缀的出现次数

原问题等价于 \(\forall i\) 求:\(f_i = \sum\limits_{j \ge i} [S_{1 \sim j} ~ has ~ a ~ border ~ of ~ S_{1 \sim i}]\)

直接暴力枚举是 \(\mathcal{O(n ^ 2)}\) 的,但注意到 \(border\) 的性质,我们从后往前递推解决这个问题:每次将已经求好的 \(i\) 的答案累加到 \(\pi_i\) 上即可做到 \(\mathcal{O(n)}\)

至于若要加强成统计 \(S\) 的每个前缀在 \(T\) 中的出现次数,只需利用 \(\rm KMP\) 的构造方式,在初始赋值时只赋 \(T\) 中位置的值即可。

统计本质不同子串的数目

直接排序 + 哈希可以做到 \(\mathcal{O(n ^ 2 \log n)}\),但存在一个不带 \(\log\) 的做法。

依然考虑递推求解,每次我们在当前字符串的末尾加入一个字符,考虑答案增量。

进一步我们转化为求加入字符后有多少个后缀出现在原来的字符串中,不难发现就是反串的 \(\max \pi\),复杂度 \(\mathcal{O(n ^ 2)}\)

字符串周期相关性质

一些定义

\(k\)\(S\) 的一个(弱)周期当且仅当 \(\forall i, i + k \le |S|, S_i = S_{i + k}\),特别地若 \(k \nmid S\) 则称 \(k\)\(S\) 的弱周期。


  1. 若字符串 \(S\) 存在一个 \(border ~ T\),当且仅当 \(S\) 存在一个长度为 \(|S| - |T|\) 的(弱)周期。

必要性显然,充分性递归论证即可。

  1. 字符串 \(S\) 的最短(弱)周期长度为 \(n - \pi_n\)

\(\pi\) 函数的定义易证。

  1. 若长度不小于 \(p + q\) 的字符串 \(S\) 存在长度分别为 \(p, q\) 的(弱)周期,那么 \(\gcd(p, q)\) 也是 \(S\) 的一个(弱)周期。

下面首先证明 \(p - q(p > q)\) 也是 \(S\) 的一个(弱)周期。

分两种情况讨论:

  1. \(i \le q\),则 \(S_i = S_{i + p} = S_{i + p - q}\),可知 \(\forall i \le q, p - q\) 是其一个(弱)周期。

  2. \(i > q\),则 \(S_i = S_{i - q} = S_{i + p - q}\),可知 \(\forall i > q, p - q\) 是其一个(弱)周期。

证明 \(p - q\)\(S\) 的一个(弱)周期后,根据更相减损术的性质,最终可以迭代至 \(\gcd(p, q)\)\(S\) 的一个(弱)周期。

  1. 所有周期的长度均为最短周期长度的倍数。

设最短周期为 \(p\),若 \(\exist q > p, p \nmid q\)\(q\) 也为一个周期,可知 \(p \mid |S|, q \mid |S|\),又 \(p, q \ne |S|\),则 \(p, q \le \frac{|S|}{2} \Rightarrow p + q \le |S|\),再根据性质 \(3\),有 \(\gcd(p, q) < p\) 也为一个周期,矛盾。

  1. 字符串 \(S\) 存在周期当且仅当 \(n - \pi_n\) 为一个周期。

必要性显然,充分性证明如下:

\(p = n - \pi_n\)\(\exist q, q\)\(S\) 的一个最小周期,那么可知 \(p < q \le \frac{|S|}{2}\),则 \(p + q < |S|\),根据性质 \(3\)\(\gcd(p, q) < q\)\(\gcd(p, q)\)\(S\) 的一个周期,矛盾。

KMP 自动机

在求 \(\pi\) 函数的时候,我们发现这个做法是支持在线插入一个字符在末尾并计算函数值的。

并且在做 \(\rm KMP\) 的过程中我们发现,在匹配串每一位求 \(\pi\) 函数时,并不关心之前匹配串的字符是什么,只关心之前的 \(\pi\) 函数值和当前位的字符。

这意味着我们可以对一个模式串建立一个自动机,每个位置上状态为当前在模式串的第几位,暴力建立这个自动机复杂度是 \(\mathcal{O(n ^ 2|\sum|)}\),因为这里不存在求 \(\pi\) 函数指针单方向移动的性质。

但实际上每次一直跳 \(\pi\) 是很浪费的,因为跳一次以后的答案之前都已经计算完毕,因此可以直接调用,于是建立自动机的复杂度就为 \(\mathcal{O(n|\sum|)}\)


\(\rm KMP\) 自动机一般用来解决特殊的匹配问题,比如:特殊字符串的匹配问题,但因为 \(\rm KMP\) 自动机用处不是很多,我也没有遇到需要的题目,在此先不再讲述。

posted @ 2020-07-10 15:57  Achtoria  阅读(210)  评论(0编辑  收藏  举报