manacher算法
前置芝士:扩展KMP(本人认为扩展KMP反而和manacher更像
0. 约定
字符串的下标从 \(0\) 开始。\(|s|\) 表示字符串 \(s\) 的长度。
对于字符串 \(s\),记其每一个字符分别为 \(s_0, s_1, \cdots, s_{|s|-1}\)。
子串 \(s_l, s_{l+1}, \cdots, s_{r-1}, s_r\) 简记为 \(s[l:r]\)。特别地,若 \(l=0\),可记作 \(s[:r]\);若 \(r=|s|-1\),可记作 \(s[l:]\)。
对于字符串 \(a, b\),\(a+b\) 表示拼接操作,即将字符串 \(b\) 拼接到字符串 \(a\) 之后,构成新的字符串。
记构成的新字符串为 \(c\),则上述拼接操作记为 \(c\gets a+b\)。
其中符号 \(x\gets y\) 表示将 \(y\) 的值赋给 \(x\)。
不论是字符还是字符串,皆不加引号。
1. 问题
目标:快速求出一个字符串的所有回文子串的长度。
首先我们进行一些预处理,是过程简单化:将原字符串的字符间插入原字符串中不存在的字符。
例:
\(\texttt{ababa}\rightarrow\texttt{~#a#b#a#b#a#}\)
\(\texttt{abaaba}\rightarrow\texttt{~#a#b#a#a#b#a#}\)
(为了方便判断越界在最前面加了另外一个字符)
可以看出,这样操作的好处是让奇回文串和偶回文串统一为了奇回文串。
奇回文串的好处是有一个中心字符,从它向两边扩展,回文串内对应的字符也相同。
这就引出了中心扩展算法。
中心扩展算法的思想非常简单,就是枚举中心字符,每次向两边能扩展就扩展,这样就求出了所有的回文子串。
但是,对于 \(\texttt{aaaaaaaaa}\) 这样的字符串,中心扩展算法的时间复杂度达到了 \(O(n^2)\),显然是不可以接受的。
因此,我们需要利用回文串的性质,对上述算法进行改进。
2. 性质
定义 \(d_i\) 为从中心字符 \(i\) 开始能扩展出的最长的回文串的长度。
我们设已经求出的一个回文串的中心为 \(o\) ,这个回文串的右端为 \(r\)。显然 \(r=o+d_o\)。
考虑 \(o\leqslant i<r\),定义 \(j=2\times o-i\) ,即 \(i\) 关于 \(o\) 的对称点。
原回文串的对称点所扩展出的回文串在原回文串内当然也是完全一致的,自然有\(d_i\geqslant \min\{r-i,d_j\}\)。
3. 算法
与扩展KMP基本 完 全 一 致。为了更好利用上述性质,我们要维护最大的 \(r\) 与对应的 \(o\)。
不知道的就暴力,能利用性质的就利用性质。
code:
for(int i=1;i<n;i++)
{
if(r>i)d[i]=min(r-i,d[2*o-i]);//性质
for(;t[i+d[i]+1]==t[i-d[i]-1];d[i]++);//暴力枚举
if(i+d[i]>r){r=i+r[i];o=i;}//更新o,r
}
时间复杂度也为\(O(n)\)。