再次学习 \(\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\) 自动机用处不是很多,我也没有遇到需要的题目,在此先不再讲述。

KMP 入门的更多相关文章

  1. zstu.4194: 字符串匹配(kmp入门题&& 心得)

    4194: 字符串匹配 Time Limit: 1 Sec  Memory Limit: 128 MB Submit: 206  Solved: 78 Description 给你两个字符串A,B,请 ...

  2. 题解报告:hdu 2087 剪花布条(KMP入门)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2087 Problem Description 一块花布条,里面有些图案,另有一块直接可用的小饰条,里面 ...

  3. hdu 1358 Period(KMP入门题)

    Period Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Subm ...

  4. KMP入门题目[不定期更新]

    HDU 1711 Number Sequence(模板题) #include <cstdio> ; ; int N, M; int textS[MAXN]; int tarS[MAXL]; ...

  5. KMP入门(匹配)

    Description Given two sequences of numbers : a[1], a[2], ...... , a[N], and b[1], b[2], ...... , b[M ...

  6. hdu 1358 period KMP入门

    Period 题意:一个长为N (2 <= N <= 1 000 000) 的字符串,问前缀串长度为k(k > 1)是否是一个周期串,即k = A...A;若是则按k从小到大的顺序输 ...

  7. hdu 1686 & poj 2406 & poj 2752 (KMP入门三弹连发)

    首先第一题 戳我穿越;http://acm.hdu.edu.cn/showproblem.php?pid=1686 题目大意好理解,每组输入一个子串和一个母串,问在母串中有多少个子串? 文明人不要暴力 ...

  8. HDU2203(KMP入门题)

    亲和串 Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submiss ...

  9. HDU2087(KMP入门题)

    剪花布条 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  10. HUD1686(KMP入门题)

    Oulipo Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Subm ...

随机推荐

  1. 全网连夜修复的Log4j漏洞,如何做到一行代码都不改?

    GitHub 21.5k Star 的Java工程师成神之路,不来了解一下吗! GitHub 21.5k Star 的Java工程师成神之路,真的不来了解一下吗! Apache Log4j2 远程代码 ...

  2. uniapp中scroll-view自定义滚动条

    注意事项 需在app.vue中添加如下,需要important /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ ::-webkit-scrollbar { width: 16upx!imp ...

  3. 使用 Android Studio 开发工具创建一个 Android 应用程序,并在 Genymotion 模拟器上运行

    需求说明: 使用 Android Studio 开发工具创建一个 Android 应用程序,并在 Genymotion 模拟器上运行 实现步骤: 打开 Android Studio,创建一个 Andr ...

  4. 编写Java程序,使用循环结构打印出九九乘法表

    编写Java程序,使用循环结构打印出九九乘法表 效果如下: 实现代码: public class Multiplication99 { public static void main(String[] ...

  5. 在本地开启了代理,postman可以正常发起外部请求,但Java代码却请求失败,已解决

    在本地开启了代理,postman可以正常发起外部请求,但Java代码却请求失败,已解决 现象:开了vpn访问公司服务器,postman可以正常发起服务器请求,但是java代码请求失败,连接超时.包括在 ...

  6. 开源社区Review代码步骤

    以Ranger项目为例,说明开源社区Review代码详细步骤. 1.寻找合适的issue进行review 首先自己需要是某个开源项目的committer, 要有合入代码的权限. 2.review代码 ...

  7. Storm集群开启HA高可用

    Storm开启HA高可用,包括Nimbus和UI开启两个及以上的进程. 基于已经安装好的Storm集群,开启关键节点角色的HA高可用. Storm安装请参考Storm集群安装Version1.0.1 ...

  8. JZOJ5409. Fantasy && Luogu2048 [NOI2010]超级钢琴

    题目大意 给出一个序列和\(L, R\), 求前k大长度在\([L,R]\)之间的连续子序列的和的和. 解题思路 朴素的想法是对于一个左端点\(p\), 它的右区间取值范围是一个连续的区间即\([p+ ...

  9. golang 开源代理

    export GOPROXY=https://goproxy.io 设置好之后就可以用go get 下载被墙的包了 项目地址:https://github.com/goproxyio/goproxy

  10. shell 之 循环执行某操作

    需求: 按月执行201904到202010的py脚本. 代码如下: #!/bin/bash i=201904 # 定义开始月份 while [ $i -le 202010 ] # 当i小于等于2020 ...