s[i:j]
表示左闭右开区间
s[i] ... s[j-1]
.
特别当 i == j
时, 表示一个空串.
可以省略左边界或右边界, 比如 s[i:]
表示一个后缀, s[:j]
表示一个前缀.
当一个前缀/后缀的长度严格小于 `s` 的长度时, 称它为 `s` 的真前缀/真后缀.
[来自 OI Wiki]
前缀函数 (prefix function) 设字符串 `s` 的长度为 `n`. 如果子串 `s[:i+1]` 有一个真前缀等于真后缀, 则称它们是 `s` 在下标 `i` 处的一个匹配对 (或 border). `s` 的前缀函数定义为一个长度为 `n` 的数组 `pi`, 其中 `pi[i]` 表示 `i` 处的最长匹配对的长度: `pi[i] := max_(0 le k le i) { k: s[0:k] = s[i-k+1:i+1] }`. 当我们顺序读取字符串 `s` 的时候, 前缀函数告诉我们, 最近读取到的几个字符如何与 `s` 的开头进行匹配.
由定义知 `pi[0] = 0`.
接下来, 我们寻找计算前缀函数 `pi` 的递推算法. 已知 `pi[i]`, 如何求 `pi[i+1]` 呢? 最好的情况是 `s[i+1] = s[pi[i]]`, 这时我们直接扩展 `i` 处的最长匹配对, 就得到 `i+1` 处的最长匹配对:
扩展性质 相邻的前缀函数值最多增加 1: `pi[i+1] - pi[i] le 1`, 等号成立当且仅当 `s[i+1] = s[pi[i]]`.
如果不巧 `s[i+1] != s[pi[i]]`, 则 `i+1` 处的最长匹配对不能由 `i` 处的最长匹配对扩展而来.
但它可能由 `i` 处的其它匹配对扩展而来. 因此我们由长到短枚举 `i`
处的所有匹配对, 判断它能否扩展为 `i+1` 处的匹配对. 如果它们全都无法扩展,
且 `s[i+1] != s[0]`, 则 `pi[i+1] = 0`.
因此, 关键问题是如何枚举 `i` 处所有匹配对. 下面的性质告诉我们, `i`
处的每个匹配对的长度可以由前面计算过的 `pi` 值得到.
可以归纳证明 `pi[i] le i`.
`j` 不是 `i` 处仅次于 `k` 的匹配对长度
`iff` 存在 `j lt m lt k`, 使得 `s[0:m] = s[i-m+1:i+1]`
`iff` 存在 `j lt m lt k`, 使得 `s[0:m] = s[k-m:k]`
`iff` `j` 不是 `k-1` 处的最长匹配对长度.
计算前缀函数
这是一个在线算法, 意思是可以逐个读取 `s` 的字符进行处理, 不必保存整个串.
设 `s` 的长度为 `n`, 则空间复杂度为 `O(n)`. 如果已知 `pi` 的最大值
`max pi = m`, 则只需存储 `pi[0:m]`, 就可满足迭代步骤 j = pi[j-1]
,
空间复杂度降低到 `O(m)`.
为了得到时间复杂度, 从迭代步骤 j = pi[j-1]
入手.
由于 `pi[j-1] le j-1`, 每迭代一次 `j` 的值都至少减小 1. 但 `j` 的值非负, 所以 `j` 减小的次数不超过
j = j+1
的执行次数, 即不超过 `n`. 因此时间复杂度为 `O(n)`.
字符串的周期
由前缀函数的定义,
字符串 `s` 的最长匹配对长度是 `pi[n-1]`, `n` 是 `s` 的长度.
那么 `n - pi[n-1]` 就是 `s` 的最小周期.
比如 abcabcabc
的最长匹配对长度等于 6, 最小周期 = 9-6 = 3.
该算法由 Knuth, Morris, Pratt 在 1977 年提出, 其核心就是前缀函数的应用.
KMP 算法
在字符串 `s` 中查找子串 `p` 出现的所有位置. 记 `s, p` 的长度分别为 `m, n`.
为此构造一个字符串 `p \# s`.
其中 `\#` 表示一个不出现在 `p` 和 `s` 中的分隔符.
计算新字符串的前缀函数, 若 `pi[i] = n`,
由定义知道存在一个右端点在 `i` 的子串与待查找的字符串 `p` 相等.
扣除 `p` 与分隔符的长度, 这个子串在 `s` 中的下标为 `i - (n-1) - (n+1) = i - 2n`.
KMP 算法的时间复杂度为 `O(m+n)`.
由于分隔符的存在, 前缀函数值不超过 `n`, 所以空间复杂度为 `O(n)`.
序列自动机
后缀自动机 (SAM)