Manacher
Manacher算法主要与回文串问题相关,能够求得以
i
i
i 为下标的回文串半径
d
p
[
i
]
dp[i]
dp[i],它相对于普通暴力算法的最关键点就是对 回文半径的初始化(重点)不仅仅是一开始为1
,而是可能从之前算出来的结果中转移
,而本文也将对其介绍。
1 算法的重点 - 半径初始化 及 分类讨论
首先解释一下上面的各个变量含义
i : 当前遍历到的下标 i,由于是从左往右遍历的,此时 [0, i - 1] 的回文串都已经求出了。
R : 当前最右边界,这一项的设置是为了保证算法的O(n)的复杂度,代码中会有解释。
图中有两个R是可能出现的两种位置:R 在当前下标 i 的左边 / 右边
idx : 当前达到最右边界的回文串中心下标
i_mirror :当前 i 关于 idx 的镜像对称位置 i_mirror,这是为了设置当前遍历下标 i 的回文串初始化长度,
可能由此镜像位置的状态进行转移
以上图为例,Manacher算法的主要思路中运用到了
d
p
(
动
态
规
划
)
dp(动态规划)
dp(动态规划) 的状态转移
思想和分类讨论
。它通过对初始化半径长度
的状态转移,以及通过设置最右边界
对后续可增加长度的最多增加限制,保证了整个算法是以
O
(
n
)
O(n)
O(n) 的复杂度不断进行(代码中会较为详细的指出)。
状态转移
:以
i
i
i 为下标的回文串初始大小不是一定从1开始,而是可能继承自 i_mirror 等长度,从而减少了复杂度。
最右边界
:到当前下标
i
i
i 为止,回文串的最右端延伸到的最远距离
R
R
R
分类讨论
:最终总共可分为3种不同的情况。我们需要从头开始讨论:
为了方便对照,此处再放一次实例图片
-
首先当前遍历到的下标 i 是确定了
-
然后 idx 和 i 的关系也能确定下来(idx < i ,因为当前达到最右边界的回文串一定是在之前求出来的,所以一定在 i 左侧)
-
再讨论 R ,会发现 R 是无法确定的(既可能半径较小,在 i 左侧;也可能半径较大,在 i 右侧;也可能刚好在 i,此时可以归为在左侧那一类 )于是我们一一往下讨论:
3.1. R 在 i 的左侧或刚好为i,此时如上图所示,没有太多的其他信息,我们找不到当前以 i 为下标的回文串初始化长度 init_length 和之前状态的关系,只能将其初始化为 0,即以 i 为下标的回文串半径初始化为他自己, d p [ i ] = 0 , w h e n R ≤ i dp[i] = 0,when \ R \leq i dp[i]=0,when R≤i
3.2 R 在 i 的右侧,此时如上图所示,说明 [ i d x , R ] [idx,R] [idx,R] 这右边半段一定和左半段是以 i d x idx idx 为中心对称的回文串,那么此时就可以看出
如上图所示,以 i i i 为中心的回文串一定和以 i m i r r o r i_{mirror} imirror 为中心的回文串(上图绿色部分)大小相同(真的一定相同吗?),即 d p [ i ] = d p [ i m i r r o r ] dp[i] = dp[i_{mirror}] dp[i]=dp[imirror]
显然有一个反例,如上图所示,可以看到此时以 i m i r r o r i_{mirror} imirror 为中心的回文串左侧已经超出了红色边界 R ′ R' R′,而我们只保证了 [ R ′ , i d x ] [R',idx] [R′,idx] 和 [ i d x , R ] [idx,R] [idx,R] (下图绿色部分)一定是对称的( [ R ′ , R ] [R',R] [R′,R]是回文串),我们无法保证剩下部分(下图紫色部分)是对称的,如下图所示
换句话说,我们只能确保以 i i i 为中心的回文串半径初始化长度在 R − i R - i R−i,或是 d p [ i m i r r o r ] dp[i_{mirror}] dp[imirror],那么为了确保一定正确,我们只需要让以 i i i 为中心的回文串初始化长度为两者的 m i n min min 即可,即 d p [ i ] = m i n ( R − i , d p [ i m i r r o r ] ) , w h e n R > i dp[i] = min(R-i,dp[i_{mirror}]), when \ R>i dp[i]=min(R−i,dp[imirror]),when R>i,再结合前面的 d p [ i ] = 0 , w h e n R ≤ i dp[i] = 0, when\ R \leq i dp[i]=0,when R≤i,可以得出一个总体的初始化长度:
d p [ i ] = R > i ? m i n ( R − i , d p [ i m i r r o r ] ) : 0 dp[i ] = R>i ? min(R-i,dp[i_{mirror}]) : 0 dp[i]=R>i?min(R−i,dp[imirror]):0
这是一个C语言的三目表达式 A ? B : C A?B:C A?B:C,其中 A = { R > i } , B = { m i n ( R − i , d p [ i m i r r o r ] ) } , C = { 0 } A = \{R>i\},B =\{min(R-i,dp[i_{mirror}])\},C = \{0\} A={R>i},B={min(R−i,dp[imirror])},C={0}翻译出来就是 R > i R>i R>i吗,如果是,则表达式的值为 B B B的值,否则为 C C C的值
2 算法的一些另外的小trick - 填充字符串
好处:对字符串进行填充,能够使奇数/偶数长度的回文串统一变成奇数长度的回文串,更为简单且减少了对边界的判断。
填充方法:abba => @#a#b#b#a#
对每个字符前面添加 # 填充,首尾再加两个不一样的字符来填充
当然填充完后,对填充后的字符串进行Manacher,相应求出的半径是带有 # 的需要转换。此时 d p [ i ] dp[i] dp[i] 的含义不变,但在数值上就等于以 i i i 为中心的回文串的长度+1,
3 代码解释 及 模板
string change(string s){
string str = "@";
for(char c : s){
str += '#';
str += c;
}
str += '#';
return str;
}
string palindrome(string s_){
s = change(s_);
int n = s.length();
vector<int>dp(n);//# 保存回文半径的长度,但经过change变换后,dp[i]其实是回文串长度+1
int R = 0, idx = 0;
for(int i = 1; i < n; i++){
//# 初始化
int i_mirror = 2 * idx - i ;
dp[i] = R > i ? min(R - i, dp[i_mirror]) : 1;
//# 有限制的暴力
while( s[i + dp[i]] == s[i - dp[i]] ) dp[i]++;
//# 对最右边界进行更新
if(i + dp[i] > R){
R = i + dp[i];
idx = i;
}
}
int maid = 1; //# 找出最长回文串的中心
for(int i = 1; i < n; i++){
maid = dp[i] > dp[maid] ? i : maid;
}
int center = (maid - 1) / 2;// # 最长回文子串的中心点
int r = (dp[maid] - 1) / 2; //# 最长回文子串的半径
return s_.substr(center - r , dp[maid] - 1);
}