Manacher
马拉车算法可以在的时间内找出一个字符串内的所有回文串。
下面讨论当所有回文串长度都为奇数的情况。对于每个位置 ,我们找出值 。这表示以位置 为中心的长度为奇数的回文串个数。换个角度,该者也表示了以位置 为中心的最长回文串的半径长度(半径长度 为从位置 到回文串最右端位置包含的字符个数)。
举例来说,字符串 以 为中心有三个奇数长度的回文串,最长回文串半径为 ,也即 :
因此关键思路是,如果以某个位置 为中心,我们有一个长度为 的回文串,那么我们有以 为中心的长度为 ,,等等的回文串。所以 数组已经足够表示字符串中所有奇数长度子回文串的信息。
朴素算法
朴素算法通过下述方式工作:对每个中心位置 ,在比较一对对应字符后,只要可能,该算法便尝试将答案加 。 该算法是比较慢的:它只能在 的时间内计算答案。
vector<int> d1(n);
for (int i = 0; i < n; i++) {
d1[i] = 1;
while (0 <= i - d1[i] && i + d1[i] < n && s[i - d1[i]] == s[i + d1[i]]) {
d1[i]++;
}
}Manacher 算法
为了快速计算所有奇数长度子回文串的情况,我们维护已找到的最靠右的子回文串的 边界 (即具有最大 值的回文串,其中 和 分别为该回文串左右边界的位置)。初始时,我们置 和 ( -1 需区别于倒序索引位置,这里可为任意负数,仅为了循环初始时方便)。
过程
现在假设我们要对下一个 计算 ,而之前所有 中的值已计算完毕。我们将通过下列方式计算:
如果 位于当前子回文串之外,即 ,那么我们调用朴素算法。
因此我们将连续地增加 ,同时在每一步中检查当前的子串 ( 表示半径长度,下同)是否为一个回文串。如果我们找到了第一处对应字符不同,又或者碰到了 的边界,则算法停止。在两种情况下我们均已计算完 。此后,需记得更新 。
现在考虑 的情况。我们将尝试从已计算过的 的值中获取一些信息。首先在子回文串 中反转位置 ,即我们得到 。现在来考察值 。因为位置 同位置 对称,我们 几乎总是可以保证 。该想法的图示如下(可认为以 为中心的回文串被「拷贝」至以 为中心的位置上):
然而有一个 棘手的情况 需要被正确处理:当「内部」的回文串到达「外部」回文串的边界时,即 (或者等价的说,)。因为在「外部」回文串范围以外的对称性没有保证,因此直接置 将是不正确的:我们没有足够的信息来断言在位置 的回文串具有同样的长度。
实际上,为了正确处理这种情况,我们应该「截断」回文串的长度,即置 。
该种情况的图示如下(以 为中心的回文串已经被截断以落在「外部」回文串内):
该图示显示出,尽管以 为中心的回文串可能更长,以致于超出「外部」回文串,但在位置 ,我们只能利用其完全落在」外部」回文串内的部分。然而位置 的答案可能比这个值更大,因此接下来我们将运行朴素算法来尝试将其扩展至「外部」回文串之外,也即标识为 "try moving here" 的区域。
经过上述操作,我们已经找到了可能的最小值。之后我们将继续运行朴素算法以尝试尽可能增加 的值。
最后,仍有必要提醒的是,我们应当记得在计算完每个 后更新值 。
Manacher 算法的实现
为了计算 ,我们有以下代码:
统一处理
以上方案仅考虑了回文串长度为奇数时的情况,但实际上可以通过一个技巧将偶数长度也归纳进来。
给定一个长度为 的字符串 ,我们在其 个空中插入分隔符 ,从而构造一个长度为 的字符串 。举例来说,对于字符串 ,其对应的 。
对于字母间的 ,其实际意义为 中对应的「空」。而两端的 则是为了实现的方便。
注意到,在对 计算 后,对于任意一个 中以位置 为中心的回文串,若其原长度为奇数,那么变换后的回文的中心依旧是 ,而若其原长度为偶数,那么变换后其长度为奇数,并且以#为中心。这样我们就只要直接计算 中奇数长度回文串的贡献即可。
同时注意到这段代码,当 时下标会溢出,我们习惯在 开头再加一个符号~,不影响结论正确性。
对于该处理过程,以下结论不难证明:
回文串长度为奇数,仅填充#
i
2i+1
s[i]
m
2m-1
s'[2i+1]
2m
4m-1
回文串长度为偶数,仅填充#
i
2i+1
s[i]
m
2m
'#'
2m+1
4m+1
回文串长度为奇数,填充#且前面加~
i
2i+2
s[i]
m
2m-1
s'[2i+2]
2m
4m-1
回文串长度为偶数,填充#且前面加~
i
2i+2
s[i]
m
2m
'#'
2m+1
4m+1
模板
https://www.luogu.com.cn/problem/P3805
Reference
最后更新于
这有帮助吗?