大家可以先去看徐毅的《浅谈回文子串问题》。
马拉车算法是一个用来求最长回文子串的线性算法,由Manacher在1975年发明。
马拉车算法的代码复杂度较回文自动机低,对于一些问题已足够。回文自动机也有码量大,空间大,侧重于处理回文后缀等问题。
这就好比SA和SAM各有自己的优势,回文自动机和Manacher在处理回文串问题也有自己的侧重。
暴力求解最长回文子串的时间复杂度为$O(n^2)$,
当然我们也可以用字符串哈希加二分做到$O(n imes log{n})$。
而 $Manacher$算法可以进一步优化到$O(n)$。
因为回文串的长度可以为奇数,也可以为偶数。如xibobix和xiboobix。
为了统一我们在每个字符两边插入一个不再字符集内的字符如‘#’,变为#x#i#b#o#b#i#x#,#x#i#b#o#o#b#i#x#。这样我们就不用分情况讨论了。
我们再引入一个新的与处理后的串等长的数组p,p_i代表以i为中心最长的回文半径。
p_i=1代表只有s这一个字符。
如:
# 1 # 2 # 2 # 1 # 2 # 2 #
1 2 1 2 5 2 1 6 1 2 3 2 1
知道了$p_i$有什么用呢?
看上面那个例子,以第二个 '1' 为中心的回文子串 "#2#2#1#2#2#" 的半径是6,而未添加#号的回文子串为 "22122",长度是5,为半径减1。
这是个巧合吗?
我们再看以第三个 '#' 为中心的回文子串 "#1#2#2#1#" 的半径是5,而未添加#号的回文子串为 "1221",长度是4,为半径减1。
总结一下:
```
定理1 以i为中心的回文串的长度为$p_i-1$
```
证明:
(1) 先考虑以字符为中心的回文子串:
如"#2#2#1#2#2#",其形式必为"#c#c……# c #……c#c#",$其半径即为原串中的半径 imes 2$。因为这是奇回文串,$所以原串长度=原串半径 imes 2 − 1$。所以原串长度等于$pi−1$.
(2) 以#为中心的回文子串:
首先在原串中这是个偶回文串。
其形式必为 "#c#c……#c # c#……c#c#",原串前一半的字符前都加一个#,长度为原串半径$ imes 2$即为原串长度。而在后面添加一个#变为新串半径所以原串长度等于新串半径$−1$即$p_i−1$。
证毕
我们现在知道了长度却不知道具体的子串。所以我们还需要知道一个回文串在原串中的开始位置。
还是来看第二个$'1'$,它在处理后的串中排第$8$,半径为$6$,$8−6=2$,正是原串中的开始位置。
我们再来看第三个$'#'$,它在处理后的串中排第$5$,半径为$5$,$5−5=0$,这可不行。我们要把中心往后移一位才行,所以们要在一开始加一个不相干的字符(如'@')。
之后再研(da)究(biao)一下发现了如下规律:
```
定理2 以i为中心的最长回文子串的开始位置为$frac{i-p_i}{2}+1$
```
这个和定理1的证明及其相似,这里不再赘述。
接下来我们看怎么求出$p$数组。
我们可以先给$p$一个下界,再通过$s_{i-p}=s_{i+p}$来增加p。
我们需要新增两个变量$mx, id$,$mx$代表现在已匹配的回文串右端点最远能到达的位置的下一位,$id$代表$mx$的中心点,显然$mx=id+p[id]$。我们还需要利用已知的$p_k$。
接下来我们开始分类讨论。
(1)$i geq mx$,这种情况我们对$[mx, i]$一无所知,只能把$p_i$设成$1$,再暴力匹配。
(2)$i < mx$,这种情况我们又得分两种情况讨论
我们找到$i$关于$id$的对称点,$id * 2 - i$,暂且记其为$j$。
I $mx-i>p_j $
显然以$j$为中心的最长回文串到不了$mx'$,这种情况我们不能确保$s_{i...mx}是否等于{s_{i-(mx-i)+1..i}}^r$,但我们可以肯定的是以$i$为中心的最长回文串的半径肯定不小于$p_j$。
由于回文所以红色部分一定等于绿色部分的翻转。有因为红色部分也是回文所以红色部分的翻转等于红色部分。
所以$p_i=p_j$。
II $mx-i leq p_j$
这种情况就比较遗憾,发现 $i+p_j-1 > mx$,这时我们的 $p_j$ 下界只能取到 $mx-i$。再暴力匹配
我们发现我们不会再mx以内在进行暴力匹配,所以暴力匹配的范围是递增的,时间复杂度是$O(n)$。
模板题:
模板题,直接上代码。
#include <iostream> #include <cstdio> #include <cstring> using namespace std; int n, n0, id, mx, res_len, res_center; int p[50000000]; char tmp[11100000], s[50000000]; void Manacher() { n0 = 0; s[++n0] = '$'; for (int i = 1; i <= n; i++) s[++n0] = '#', s[++n0] = tmp[i]; s[++n0] = '#'; id = mx = res_len = res_center = 0; for (int i = 1; i <= n0; i++) { p[i] = mx > i ? min(mx - i, p[id * 2 - i]) : 1; while (s[i + p[i]] == s[i - p[i]]) ++p[i]; if (mx < i + p[i]) mx = i + p[i], id = i; if (res_len < p[i]) res_len = p[i], res_center = i; } } int main() { scanf("%s", tmp + 1); n = strlen(tmp + 1); Manacher(); cout << res_len - 1; return 0; }
习题:
因为时间有限,所以只有部分题目配有讲解,如有需要,请在评论区留言,谢谢!