字符串算法(自动机无关)

KMP 算法 和 border 理论

\(border(S)\) 表示字符串 \(S\) 的公共真前后缀集合.KMP 算法可以用来求一个字符串每个前缀的最大 \(border\),记第 \(i\) 个前缀的最大 \(border\)\(f_i\)

\(f_i\) 肯定为第 \(prefix_{i-1}\)\(border\) 在两个右端添加 \(c_i\) 得到的.我们在失配的时候暴力跳 \(f_{f_{i-1}}\) 即可.

由上述算法可以看出存在结论:\(\sum |f_i-f_{i-1}|\) 均摊是 \(O(|S|)\) 的.这在一些使用或者改造 KMP 的场合是值得注意的.

KMP 自动机?感觉功能被 AC 自动机平替了.没用.

失配树?感觉就是 AC 自动机的 Fail Tree 啊.没用吧.

border 理论

border 和周期的关系

很显然都结论:串 \(S\) 若存在 \(border\) 长度为 \(i\) 等价于存在周期 \(|S|-i\)

把周期的要求写出来就明了了:
\(S\) 存在一个长度为 \(i\) 的周期,就是要求 \(\forall j\le |S|-i,S_j=S_{j+i}\)

考虑存在长度为 \(i\)\(border\) 就是有 \(\forall j\le i,S_j=S_{|S|-i+j}\).这个和存在一个长度为 \(|S|-i\) 的周期是等价的.

border 的性质

若一个字符串存在 \(border\),则最小的 \(border\) 不会超过字符串长度的一半.

证明:若存在 \(border\) 存在相交,则相交的部分一定也是 \(border\),并且其长度一定比原来的 \(border\) 小.那么我们不断取 \(border\) 相交的部分作为新的 \(border\),最终得到的 \(border\) 一定不大于原串的一半.

Manacher 算法

感觉没什么用啊.有什么事写二分+哈希就好了.

功能

求出字符串每一处的回文半径,记为 \(p_i\)

实现方法

manacher 只能处理存在回文中心(长度为奇数)的回文串.故需要在待处理串 \(T\) 的字符空隙和开头结尾添加 相同 的特殊字符 \(ch_1\) 得到 \(S_1\).并且为了防止算法运行溢出边界,再在 \(S_1\) 的开头结尾分别添上特殊字符 \(ch_2,ch_3\) 得到最终可以进行 manacher 算法的字符串 \(S\)
注意需保证 \(ch_1,ch_2,ch_3\) 各不相同.

顺序扫描到位置 \(i\),记录回文中心 \(< i\) 的右端点最大的回文串右端点 \(r\) 和其回文中心 \(d\)
分情况讨论求解 \(p_i\)

  • \(r<i\).令 \(p_i \leftarrow i\)
  • \(r \ge i\).令 \(p_i \leftarrow \min(r-i+1,p_{d*2-i})\)

在第二种情况中,由于 \(S[d*2-r, r]\) 是回文串,所以 \(i\) 沿 \(d\) 对称的位置 \(d*2-i\) 作为回文中心,在 \(S[d*2-r, r]\) 内的回文串对称到 \(i\) 上不变,同样是回文串.

然后我们在这个基础上扩展 \(p_i\) 直到两端的字符不相等(这里就发挥了 \(S\) 两端限制字符 \(ch_1,ch_2\) 的防止溢出作用).

在求解出 \(p_i\) 后,用 \(i+p_i-1,i\) 尝试更新 \(r,d\)

时间复杂度分析

考虑求解 \(p_i\) 时:

  • \(r<p_i\)\(p_i\) 每扩展一步,\(r\) 都会增加.
  • \(r \ge p_i\)
    • \(p_{d*2-i} \le r-i+1\)\(p_i\) 不会扩展.
    • \(p_{d*2-i} > r-i+1\)\(p_i\) 每扩展一步,\(r\) 都会增加.

这说明,\(p\) 数组扩展的总次数等价于 \(r\) 增加的总次数.而 \(r\) 至多从 \(0\) 增加到 \(|S|\).共算法总时间复杂度为 \(O(|S|)\).非常的有力啊.

扩展 kmp(z 算法)

感觉没什么用啊.有什么事写二分+哈希可以.

用处

  1. 求出 \(S\) 的每一个后缀和 \(S\) 的 lcp.
  2. 求出另一个串 \(T\) 的每一个后缀和 \(S\) 的 lcp.

算法流程

首先先考虑第一个问题.记 \(suf_i\) 表示 \(S\)\(i\) 开头的后缀.设 \(z\) 数组,\(z_i\) 表示 \(S\)\(suf_i\) 的 lcp 长度.然后我们从前往后扫,求出数组 \(z\)

当扫描到 \(i\) 时,维护 \(l=j,r=z_j(j\le i)\) 满足 \(z_j\) 最大.考虑 \(z_{i+1}\)

\(i+1>r\),则使得 \(0\rightarrow z_{i+1}\).否则使得 \(\min(r-i+1,z_{i-l+1})\rightarrow z_{i+1}\)

这样赋值之后尝试暴力更新 \(z_{i+1}\) 直到不能更新即可.然后我们尝试使用 \(i+1,z_{i+1}\) 去更新 \(l,r\)

然后第二个问题的求解方法和上述流程基本一样.以至于存在一种偷懒的方法是将两个字符串添加一个特殊字符然后拼起来.然后对整个大串求 \(z\) 数组.

值得注意的一点是,一开始不能把 \(l\) 更新成 \(1\).我们算法保证时间复杂度关键的一点是可以用 \(z_{i-l+1}\) 快速地更新 \(z_i\).而当 \(l\) 一开始赋成 \(1\) 后,\(i-l+1\) 恒等于 \(i\).我们的算法就没有意义了.

算法复杂度分析

考虑每一次暴力更新 \(z_{i+1}\),都会使得 \(r\) 向右扩展.故暴力扩展的次数上界是 \(O(|S|)\) 的.

一些闲言碎语.感觉跟什么 manacher 很像.

posted @ 2023-07-14 00:02  ckain  阅读(40)  评论(0)    收藏  举报