Manacher 备忘录
一直记不住的 马拉车 板子. 前几天校内选拔赛还不会写, 惨遭吊打.
综述
对于一个长度为 $n$ 的字符串, Manacher 可以在 $O(n)$ 的时间内对每个位置 $i$ 计算一个类似于最长回文半径的东西.
具体而言, Manacher 在扫描每个位置的时候, 维护一个右端点最靠右的回文串的左右端点, 并借助于这个回文串来计算需要的信息.
回文性质的描述
记 $d_1(i)$ 表示以位置 $i$ 为中心的长度为奇数的回文串个数, 那么以 $i$ 为中心的最长回文串长度即 $2d_1(i) - 1$.
记 $d_2(i)$ 表示, 以位置 $i$ 和 $i-1$ 为中心的长度为偶数的回文串个数, 同样可以得到以 $i$ 和 $i-1$ 为中心的最长回文串长度为 $2d_2(i)$. 容易发现 $d_2(1)$ 是没有意义的.
$d_1$ 与 $d_2$ 的计算
首先来考虑 $d_1$. 对于一个长度为 $n$ 的字符串 $s$, 枚举每个位置 $i$, 并维护右端点最靠右的回文串的左右端点 $l$ 和 $r$.
优先考虑 $i < r$ 的情况. 此时对于一个位置 $i$, 可以在维护的这个回文串中得到一个对称的位置 $x$, 对于正整数 $k$, 如果 $s_{x-k}$ 到 $s_{x+k}$ 是回文串, 那么 $s_{i-k}$ 到 $s_{i+k}$ 同样是回文串.
由此可以得到
容易发现 $x$ 就是 $l + r - i$. 随后需要继续暴力维护 $d_1(i)$ 以保证其正确性.
对于 $i \geq r$ 的情况, 上面的讨论显然是不能沿用的, 暴力计算就好了.
1 | for (int l = 1, r = 0, i = 1; i <= n; ++i) { |
$d_2$ 的情况类似, 此处直接给出代码实现
1 | for (int l = 1, r = 0, i = 1; i <= n; ++i) { |
时间复杂度
考虑 Manacher 维护的右端点 $r$ 在整个过程中单调不减, 而每次大力计算 $d$ 的过程至少令 $r$ 后移一位, 可以感性认识到时间复杂度为 $O(n)$.
偶数长度的简化处理
如果在字符串的每个字符之间都插入一个相同的特殊字符, 就可以把 $d_2$ 的计算转化为 $d_1$ 的情况. 具体而言, 对原来的字符串做这样的变换 abbaab --> #a#b#b#a#a#b#
. 此外, 为了简化边界的情况, 往往在前面增加一个更特殊的字符, 即 abbaab --> $#a#b#b#a#a#b#
.
之前一直是这样写 Manacher 的, 优点是实现起来简单, 不过常数也大一点.
例题
Luogu P3805 【模板】manacher 算法
题目链接
解题思路
作为 Manacher 的模板题, 自然是不会用 Manacher 写的 (
首先是经典的求一个字符串中最长回文子串长度, 此时可以对字符串正反做两次哈希然后二分, 时间复杂度 $O(n\log n)$.
在忘记 Manacher 怎么写的时候很实用.
事实上存在时间复杂度为 $O(n)$ 的做法, 不过没有单纯用二分直接. 大体上是对每个位置 $i$, 维护位置 $i$ 结尾的最长回文串长度 $R(i)$. 同时 $R(i)$ 存在性质 $R(i)\le R(i - 1) + 2$. 借助与这个性质, 暴力计算 $R(i)$ 的时间复杂度就可以做到 $O(n)$.
详见 https://oi-wiki.org/string/hash/#_4.
不过这道题有点卡常, 我写的双模数哈希的 $O(n)$ 做法 TLE 了, 换成自然溢出就过了, 但显然后者是不可取的 (
Luogu P4555 [国家集训队]最长双回文串
题目链接
解题思路
在最开始的时候提到了一点, Manacher 维护了一个右端点最靠右的回文串.
记 $f(i)$ 表示以位置 $i$ 结尾的最大回文串长度, $g(i)$ 表示以位置 $i$ 开头的最大回文串长度, 那么所求即
$f(i)$ 和 $g(i)$ 可以借助 $d_1$, $d_2$ 计算出来. 时间复杂度 $O(n)$.
代码实现
听说”随手开一大块数组就像随地大小便”, 不管了 (
1 | // Luogu P4555 |
后记
看完 OI-Wiki 的评论之后, 似乎 Manacher 和 Z Algorithm 有千丝万缕的联系.
事实上两者都维护了一个右端点最靠右的满足某个性质的子串, 然后利用这个子串来计算接下来一个位置对应值. Manacher 维护的是回文串, Z Algorithm 则是一个前缀.
既然新生赛出了个 Z Algorithm, CCCC 校内选拔出了个 Manacher, 那么以后会不会继续出字符串板子呢