KMP 做题心得

posted on 2023-08-26 03:50:59 | under 笔记 | source

upd on 2026:远古文章,较为简单且可能有误。

前言

本文是写给自己看的,部分内容比较枯燥模糊。

\(\rm KMP\) 的作用

用于模式串匹配问题 废话。但是更多题目中关注的是其失配数组,因为失配数组 \(fail_i\) 等价于字符串 \(S\) 长度为 \(i\) 的前缀的最大 \(\rm border\)

也就是说,借用 \(\rm KMP\) 的副产物 \(fail_i\) 可以解决很多与 \(\rm border\) 有关的问题。

  • 性质 \(1\)

    定义 \(p\)\(S\) 的一个周期,当且仅当 \(\forall i,S_i=S_{i+p}\)

    整周期:\(p\) 整除 \(|S|\),则称 \(p\)\(S\) 的一个整周期。

    那么就有 \(p=|S|-border(S)\),也就是说每个 \(\rm border\) 都对应着一个周期。

    其中,\(\max p=|S|-\min border(S)\)\(\min p=|S|-\max border(S)\)

  • 例题:

    • P4391 [BOI2009] Radio Transmission

      求的是 \(\min p(S)\),不要求整周期。

      借助性质 \(1\),直接输出 \(n-fail_n\) 即可。

      测评记录

    • P3435 [POI2006] OKR-Periods of Words

      \(\sum \max p(S[1,i])\to \sum i-\min border(S[1,i])\)

      显然可以一直跳 \(fail\),也就是 \(j=fail_j\) 不断迭代,直到 \(fail_j=0\) 此时 \(j\) 就是 \(\min border\)

      直接写会超时。观察到 \(fail_i\) 会被遍历多次,那么考虑记忆化,直接令 \(fail_i=min border(S[1,i])\) 即可。

      测评记录

  • 性质 \(2\)

    定义 \(\rm BD(S)\) 表示 \(S\) 的所有 \(\rm border\) 集合,那么 \(\rm BD(S)=\max border(S)+BD(\max border(S))\)

  • 例题:

    • P5829 【模板】失配树

      万物之根源。

      题意:求两前缀的公共最长 \(\rm border\)

      观察性质 \(2\),我们发现任意 \(\rm border\) 都可被表示为 \(\dots fail_{fail_{fail_{n}}}\) 的嵌套模式,也就是说第 \(i\)\(\rm border\) 可被表示为 \(fail\)\(i-1\)\(\rm border\)

      利用这一点,我们把不妨让 \(fail_i\to i\) 连一条边,建成的树就叫失配树(\(fail\) 树)。需要注意的是,失配树是由叶子节点指到根节点的,也就是说根为 \(0\),而 \(n\) 在最底层。

      于是两前缀的公共 \(\rm border\) 实际上就是树上对应两点的公共祖先,由于从叶子走到根前缀大小不断递减,所以最大公共 \(\rm border\) 就是两点的 \(\rm LCA\)

      那么建树跑 \(\rm LCA\) 即可。不过此题没必要显性的建树,因为倍增法只关注祖先是谁,所以令 \(jump_{(i,0)}=fail_i\) 即可。

      测评记录,往后遇到部分问题时就可以在放在树上思考了。

    • P2375 [NOI2014] 动物园

      求所有前缀的不重叠 \(\rm border\)

      容易想到:找到最长不重叠 \(\rm border\) 然后向上跳 \(fail\) 并记录。

      可以优化向上跳这一步骤,这部分的答案实际上是失配树上对应点到根这条链上的点数(没有根)。

      不过还是会超时。显然成为不重叠 \(\rm border\) 的前提是它是 \(\rm border\),于是仿照求 \(fail\) 的步骤,之后直接跳 \(fail\) 找不重叠 \(\rm border\) 即可。注意 \(j\) 的值是不断传递的。

      为什么传递 \(j\) 就好了?考虑 \(S[1,i-1]\) 的一个重叠 \(\rm border\) \(k\),现在要拓展一位到 \(S[1,i]\)。那么无论如何,经过上述操作后,\(k\) 的最大合法结果一定与 \(j\) 的最大合法结果相同(自己试试)。也就是说 \(j\) 可递推得到的合法结果一定包含了所有合法结果。并且这样做 \(j\) 每次至多增加 \(1\),复杂度是均摊正确的。

      上面的优化就是 \(\rm KMP\) 的重要思路:传递 \(j\) 值,尽量避免重复状态。

      测评记录,注意细节。

    • P3426 [POI2005] SZA-Template

      容易发现 \(S\) 的印章一定是 \(S\)\(\rm border\),利用这一点直接暴力判断每个 \(\rm border\) 是否可以覆盖 \(S\) 即可。

      (暴力可过,但也能被卡)

      暴力不行,考虑 \(\rm DP\),令 \(dp_i\) 表示 \(S\) 长度为 \(i\) 的前缀的最小印章长度。

      易发现,\(dp_i\) 的取值要么是 \(i\)、要么是 \(dp_{f_i}\)。对于后者,\(dp_j,j\ne BD(S[1,i])\) 的状态显然无法转移。又由性质知 \(dp_{f_i}\) 包含了 \(dp_j,j=BD(S[1,i])\) 所有情况,因此转移它不会漏解。

      当然 \(S[1,i]\) 的最长 \(\rm border\) 不一定能覆盖住 \(S[1,i]\),因此再定义 \(h_{len}\) 表示长度为 \(len\) 的印章最多能完全覆盖到哪,若 \(h_{f_i}\ge i-f_i\) 则可以 \(dp_{f_i}\to dp_i\),最后用 \(i\) 更新 \(h_{dp_i}\) 即可。

      (我个人感觉这种做法存在瑕疵,因为它的转移过程中只关注最小的印章,如果有最小的印章不能覆盖但次小印章可以覆盖的情况,那么上述做法是错的)

      好了好了,现在讲讲真正的正解:

      先将 \(S\) 的所有 \(\rm border\) 从小到大存起来,定义它们为 \(border_1...border_p\),然后逐个判断直到满足条件(暴力),需要优化。

      然后发现 \(border_i\) 可以继承前面的信息。由于 \(border_i\) 是合法印章的条件是 \(S\)\(border_i\) 的匹配位置的最大间距不超过 \(border_i\),那么考虑双向链表维护最大间距。

      于是问题转变为链表的维护,也就是删去上一个 \(border\) 可以匹配,但当前 \(border\) 无法匹配的位置。这样会漏解吗?也就是是否会出现 \(border_{i-1},border_{i+1}\) 可以匹配,但 \(border_{i}\) 无法匹配的情况吗?显然不会,因为 \(border_{i}\)\(border_{i+1}\) 的一个前缀,若后者可以匹配,则前者也可以匹配;后者无法匹配的,前者有可能可匹配,这就是维护操作的由来。

      最后的问题是怎么快速找到 \(border_i\) 不可匹配,但 \(border_{i-1}\) 可匹配的位置。

      继续观察,然后我们发现,若定义 \(S\)\(border_i\) 的一个匹配位置(右端点)为 \(j\),则必须满足 \(border_i\)\(S[1,j]\) 的一个 \(\rm border\)。这是显然的,因为 \(border_i\) 能完全覆盖 \(S[1,j]\) 的前提之一是 \(S[1,border_i-1]=S[j-border_i+1,j]\)

      联系失配树的概念,\(border_i\)\(S\) 中的成功匹配位置恰好对应着失配树中 \(border_i\) 的子树中的节点。

      而且失配树还有这样的性质:不同子树中的两节点 \(x,y\),其中 \(x\) 子树中的节点对应的前缀的 \(\rm border\) 不可能是 \(y\) 所对应的 \(\rm border\)(有点绕一定要仔细看看)。放在此题中就意味着 \(x\) 子树中的节点所对应的位置可以被 \(x\) 匹配,但不能被 \(y\) 匹配。

      又因为 \(border_i\) 的父节点是 \(border_{i-1}\)。所以 \(border_i\) 不可匹配,但 \(border_{i-1}\) 可匹配的位置就是 \(border_{i-1}\) 的子树中除 \(border_i\) 这部分的所有节点。

      所以只需从失配树的根跑到 \(border_p\),不断删除链表中的点,并判断最大间隙是否 \(\le border_i\)

      走的是一条链,每次维护查询都是 \(O(1)\),所以复杂度为 \(O(n)\)

      写题心路:

      第一眼:有点意思,不愧是 POI 的题。

      写出暴力并过了后:???什么水数据。

      写完 DP 后:有点意思但就这?

      发现 DP 瑕疵后:什么玩意?那该咋做?

      发现暴力甚至没打对还玄学过了后:数据 666。

      钻研正解时:好难啊 woc。

      写出来后:好耶ヽ(✿゚▽゚)ノ

      附上正解的提交记录

  • 杂题

    • P4824 [USACO15FEB] Censoring S

      每个字符显然只会被删除、计算一次,那考虑用栈存储文本串。

      那么怎么处理删掉后新出现的串呢?直接对文本串处理不好做,那么考虑让模式串的指针直接移到相应位置。

      如样例中 \(momoo\) 这部分,我们删除 \(moo\) 后,直接让 \(j\) 移到之前匹配 \(mo\) 时所到的位置,也就是说接下来令 \(o\)\(T_{3}=o\) 匹配即可。

      具体实现的话,定义数组 \(p_i\) 记录文本串到第 \(i\) 位时,对应的模式串中所匹配到的位置。

      也就是删除 \(S[i-m+1,i]\) 时,弹出栈前 \(m\) 个元素。现在栈顶元素就是文本串删除操作后的最后一个字符,让 \(j\) 移到 \(p_{st[top]}\) 即可。

      看代码吧,这种思路还是很值得学习的。

posted @ 2026-01-14 17:34  Zwi  阅读(1)  评论(0)    收藏  举报