S001 【模板】从前缀函数到KMP应用 字符串匹配 字符串周期

这篇博客为总结的解题流程和模板 ,如果想要算法具体的原理和数学证明的话请参考:Prefix function. Knuth--Morris--Pratt algorithm

最长相等真前后缀

最长相等前后缀也被称为 Border ,KMP算法就利用了其性质来进行匹配优化。

  • 前缀:从字符串第一个字符开始的子串。
  • 后缀:以字符串最后一个字符结束的子串。
  • 真:长度严格小于原字符串长度。

求法( \(O(n^2)\) ):

python 复制代码
def border(s):
    n = len(s)
    L = 0
    for i in range(1, n):  # 长度为 i 上限为 n-1 确保为真前后缀
        if s[:i] == s[n-i:n]:
            L = i
    return L

还有更加高效的算法,可以优化到 \(O(n)\) 。正是这个优化产生了 KMP 算法。

前缀函数

在 KMP 中的前缀函数的到数组 \(\pi\) ,其中 \(\pii\) 表示字符串的前缀 \(t0\\dots i\) 中,最长的相等真前后缀的长度。

如果使用暴力枚举每个子串进行一次 border 函数的话时间复杂度是 \(O(n^3)\)

python 复制代码
b = []
for i in range(len(s)):
    b.append(border(s[:i + 1])) 

通过递推可以在 \(O(n)\) 时间内求出前缀数组 \(\pi\)

  1. 假设我们正在计算 \(\pii\) ,并且已知 \(\pii-1=j\) 。
  2. 这意味着 \(t0\\dots j-1\) 是 \(t0\\dots i-1\) 的最长相等前后缀。
  3. 情况A:如果 \(ti==tj\) ,那么 \(\pii = j+1\) 。
  4. 情况B:如果 \(ti \ne tj\) ,我们需要一个更短的相等前后缀。于是我们可以让 \(j=\pij-1\) ,然后重复 \(ti\) 和 \(tj\) 的比较过程,直到匹配或 \(j\) 降为 \(0\) 。

具体代码为

python 复制代码
def get_pi(s):
    n = len(s)
    pi = [0] * n

    for i in range(1, n): 
        j = pi[i - 1]  # 取前一个的位置的pi

        while j > 0 and s[i] != s[j]:  # 情况B
            j = pi[j - 1]
        if s[i] == s[j]:  # 情况A
            j += 1
        pi[i] = j

    return pi

应用

模式串匹配

已知模式串 \(t\) 和匹配串 \(s\) ,在预处理完模式串的 \(\pi\) 数组后可以通过双指针匹配。

  1. 指针 \(i\) :始终在 \(s\) 上向右移动,不回退。
  2. 指针 \(j\) :在 \(t\) 上移动,如果匹配 j++ 如果失配 \(j\) 根据 \(\pi\) 数组向左跳,跳到一个可以让前面部分继续匹配的位置。

这个根据 \(\pi\) 数组向左跳的过程可以理解成下面这句有名名的话:

一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。

python 复制代码
m, n = len(t), len(s)
pi = get_pi(t)

j = 0  # 模式串指针
for i in range(n):  # 文本串指针永不回退
    while j > 0 and s[i] != t[j]:
        j = pi[j - 1]

    if s[i] == t[j]:
        j += 1

    if j == m:  # 匹配成功
        # 位置为 i - j + 1 0-based
        j = pi[j - 1]  # 继续匹配可能重叠的下一处

求字符串周期

先利用预处理的 \(\pi\) 数组求出所有的 Border ,再根据这些 Border 就可以构造出所有的周期串了。

求字符串所有周期

由于 \(\pi\) 数组记录了最长 Border ,而次长的 Border 可能通过 \(\pi\\pi\[n-1-1]\) 递归求得,因此我们可以不断回跳,求出所有的 Border 后,周期就是 n - Border。

python 复制代码
n = len(s)
pi = get_pi(s)

b = []
k = pi[n - 1]
while k:
    b.append(n - k)
    k = pi[k - 1]
b.append(n)

print(*b)

不要忘记了 \(s\) 自身也是周期,然后每个周期串就是 s[:b[i]]

完全循环

在 \(\pin-1>0\) 基础上,当它的最小正周期 \(n-\pin-1\) 可以被总长度 \(n\) 整除时,存在完全循环。

python 复制代码
n = len(s)
pi = get_pi(s)

k = n - pi[n - 1]
print("YES" if pi[n - 1] > 0 and n % k == 0 else "NO")

例题

1753 String Matching - CSES 模式串匹配模版

1732 Finding Borders - CSES 求出字符串所有 Border

1733 Finding Periods CSES 求出所有的周期大小

459. 重复的子字符串 - 力扣 判断是否存在完全循环

相关推荐
一只小菜鸡..6 小时前
南京大学 操作系统 (JYY) 学习笔记:从 UNIX 到 Linux 与庞大的应用生态
笔记·学习·unix
熊猫钓鱼>_>8 小时前
ArkTS 装饰器总览:V1 / V2 / 通用装饰器完整学习笔记
笔记·学习·华为·交互·harmonyos·arkts·arkweb
GLDbalala9 小时前
GPU PRO 5 - 5.2 glTF: Designing an Open-Standard Runtime Asset Format笔记
笔记
春生野草9 小时前
个人笔记——栈
笔记
chnyi6_ya9 小时前
论文阅读笔记 | One Sentence, One Drama: 基于多智能体系统的个性化短剧生成
论文阅读·笔记
ysa05103010 小时前
【板子】短序列dp(换成维护更小常数维度的dp)
c++·笔记·算法·板子
Dory_Youth11 小时前
联想笔记本电脑失灵
运维·笔记·电脑
2501_9269783311 小时前
认知边缘的双向耦合:从核技术类比到智力货币时代
人工智能·经验分享·笔记·ai写作
hanlin0311 小时前
刷题笔记:力扣第242、349题(哈希表)
笔记·算法·leetcode
茯苓gao12 小时前
嵌入式开发笔记:CAN与CAN FD完全指南——从帧格式差异到MCU选型实战
网络·笔记·stm32·嵌入式硬件·学习·汽车