Manacher

马拉车算法可以在O(n)O(n)的时间内找出一个字符串内的所有回文串。

下面讨论当所有回文串长度都为奇数的情况。对于每个位置 i=0n1i = 0 \dots n - 1 ,我们找出值 d1[i]d_1[i] 。这表示以位置 ii 为中心的长度为奇数的回文串个数。换个角度,该者也表示了以位置 ii 为中心的最长回文串的半径长度(半径长度 d1[i]d_1[i]为从位置 ii 到回文串最右端位置包含的字符个数)。

举例来说,字符串 s=abababcs = \mathtt{abababc}s[3]=bs[3] = b 为中心有三个奇数长度的回文串,最长回文串半径为 33,也即 d1[3]=3d_1[3] = 3

a b a bs3 a bd1[3]=3 ca\ \overbrace{b\ a\ \underset{s_3}{b}\ a\ b}^{d_1[3]=3}\ c

因此关键思路是,如果以某个位置 ii 为中心,我们有一个长度为 ll 的回文串,那么我们有以 ii 为中心的长度为 l2l - 2l4l - 4,等等的回文串。所以 d1[i]d_1[i]数组已经足够表示字符串中所有奇数长度子回文串的信息。

朴素算法

朴素算法通过下述方式工作:对每个中心位置 ii,在比较一对对应字符后,只要可能,该算法便尝试将答案加 11。 该算法是比较慢的:它只能在 O(n2)O(n^2) 的时间内计算答案。

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 算法

为了快速计算所有奇数长度子回文串的情况,我们维护已找到的最靠右的子回文串的 边界 [l,r][l, r](即具有最大 rr 值的回文串,其中 llrr 分别为该回文串左右边界的位置)。初始时,我们置 l=0l = 0r=1r = -1-1 需区别于倒序索引位置,这里可为任意负数,仅为了循环初始时方便)。

过程

现在假设我们要对下一个 ii 计算 d1[i]d_1[i],而之前所有 d1[]d_1[] 中的值已计算完毕。我们将通过下列方式计算:

  • 如果 ii 位于当前子回文串之外,即 i>ri > r,那么我们调用朴素算法。

    因此我们将连续地增加 d1[i]d_1[i],同时在每一步中检查当前的子串 [id1[i]i+d1[i]][i - d_1[i] \dots i + d_1[i]]d1[i]d_1[i] 表示半径长度,下同)是否为一个回文串。如果我们找到了第一处对应字符不同,又或者碰到了 ss 的边界,则算法停止。在两种情况下我们均已计算完 d1[i]d_1[i]。此后,需记得更新 [l,r][l, r]

  • 现在考虑 iri \le r 的情况。我们将尝试从已计算过的 d1[]d_1[] 的值中获取一些信息。首先在子回文串 [l,r][l, r] 中反转位置 ii,即我们得到 j=l+(ri)j = l + (r - i)。现在来考察值 d1[j]d_1[j]。因为位置 jj 同位置 ii 对称,我们 几乎总是可以保证 d1[i]d1[j]d_1[i] \ge d_1[j]。该想法的图示如下(可认为以 jj 为中心的回文串被「拷贝」至以 ii 为中心的位置上):

     sl  sjd1[j]+1  sj  sj+d1[j]1palindrome  sid1[j]+1  si  si+d1[j]1palindrome  srpalindrome \ldots\ \overbrace{ s_l\ \ldots\ \underbrace{ s_{j-d_1[j]+1}\ \ldots\ s_j\ \ldots\ s_{j+d_1[j]-1} }_\text{palindrome}\ \ldots\ \underbrace{ s_{i-d_1[j]+1}\ \ldots\ s_i\ \ldots\ s_{i+d_1[j]-1} }_\text{palindrome}\ \ldots\ s_r }^\text{palindrome}\ \ldots

    然而有一个 棘手的情况 需要被正确处理:当「内部」的回文串到达「外部」回文串的边界时,即 jd1[j]+1lj - d_1[j] + 1 \le l(或者等价的说,i+d1[j]1ri + d_1[j] - 1 \ge r)。因为在「外部」回文串范围以外的对称性没有保证,因此直接置 d1[i]=d1[j]d_1[i] = d_1[j] 将是不正确的:我们没有足够的信息来断言在位置 ii 的回文串具有同样的长度。

    实际上,为了正确处理这种情况,我们应该「截断」回文串的长度,即置 d1[i]=rid_1[i] = r - i

    该种情况的图示如下(以 jj 为中心的回文串已经被截断以落在「外部」回文串内):

     sl  sj  sj+(jl)palindrome  si(ri)  si  srpalindromepalindrome try moving here\ldots\ \overbrace{ \underbrace{ s_l\ \ldots\ s_j\ \ldots\ s_{j+(j-l)} }_\text{palindrome}\ \ldots\ \underbrace{ s_{i-(r-i)}\ \ldots\ s_i\ \ldots\ s_r }_\text{palindrome} }^\text{palindrome}\ \underbrace{ \ldots \ldots \ldots \ldots \ldots }_\text{try moving here}

    该图示显示出,尽管以 jj 为中心的回文串可能更长,以致于超出「外部」回文串,但在位置 ii,我们只能利用其完全落在」外部」回文串内的部分。然而位置 ii 的答案可能比这个值更大,因此接下来我们将运行朴素算法来尝试将其扩展至「外部」回文串之外,也即标识为 "try moving here" 的区域。

    经过上述操作,我们已经找到了d1[i]d1[i]可能的最小值。之后我们将继续运行朴素算法以尝试尽可能增加 d1[i]d_1[i] 的值。

最后,仍有必要提醒的是,我们应当记得在计算完每个 d1[i]d_1[i] 后更新值 [l,r][l, r]

Manacher 算法的实现

为了计算 d1[]d_1[],我们有以下代码:

统一处理

以上方案仅考虑了回文串长度为奇数时的情况,但实际上可以通过一个技巧将偶数长度也归纳进来。

给定一个长度为 nn 的字符串 ss,我们在其 n+1n + 1 个空中插入分隔符 #\#,从而构造一个长度为 2n+12n + 1 的字符串 ss'。举例来说,对于字符串 s=abababcs = \mathtt{abababc},其对应的 s=#a#b#a#b#a#b#c#s' = \mathtt{\#a\#b\#a\#b\#a\#b\#c\#}

对于字母间的 #\#,其实际意义为 ss 中对应的「空」。而两端的 #\# 则是为了实现的方便。

注意到,在对 ss' 计算 d1[]d_1^{'}[] 后,对于任意一个 ss 中以位置 ii 为中心的回文串,若其原长度为奇数,那么变换后的回文的中心依旧是 s[i]s[i] ,而若其原长度为偶数,那么变换后其长度为奇数,并且以#为中心。这样我们就只要直接计算 ss' 中奇数长度回文串的贡献即可。

同时注意到这段代码,当 i=0 and k=1i=0 \ and\ k=1 时下标会溢出,我们习惯在 ss' 开头再加一个符号~,不影响结论正确性。

对于该处理过程,以下结论不难证明:

变换类型
i
i'
原对称中心
d[i]
原以i下标为中心的极大回文串的长度(可奇可偶)
现对称中心
d1'[i']
现对应极大回文串的长度(奇数)

回文串长度为奇数,仅填充#

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

https://oi-wiki.org/string/manacher

最后更新于

这有帮助吗?