文章目录
马拉车算法(Manacher’s Algorithm)
解决的问题是求最长回文子串,神奇之处在于将算法的时间复杂度精进到了O(N)。
重点就是高效地计算回文半径数组
p
p
p,
p
p
p 中每一个元素表示以该字符为中心的最长半径(不包括该中心位置),那么这个半径就是指的以该字符为中心的最长回文子串的长度,然后再通过 int start_index = (i-p[i]) / 2
推算出最长回文子串的起始位置,便可以截取最长回文子串了。
算法由来
在求解最长回文子串的问题时,可以通过中心扩展法来求解:
- 从一个中心位置开始,向其左右两边扩展寻找最长回文。
- 考虑到子串的长度可能为奇数也可能为偶数,
- 中心位置在字符串字符上时,子串是奇数的;
- 中心位置在两个字符之间时,子串是偶数的;
- 设字符串长度为 n n n,那么需要考虑的中心位置共有 2 n − 1 2n-1 2n−1个,需要从每个中心位置向外扩展,寻找以该位置为中心的最长回文子串。
这种解法的时间复杂度是 O ( N 2 ) O(N^2) O(N2),那么能不能将时间复杂度再降低一点?做到线性?马拉车算法就完美地解决了这个问题。
Manacher 算法本质上还是 中心扩散法 ,只不过它使用了类似 KMP 算法的技巧,充分挖掘了已经进行回文判定的子串的特点,提高算法的效率。
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
if(n < 2) {
return s;
}
int longest_start = 0;
int longest_end = 0;
int max_length = 0;
for(int i=0; i<n; i++) {
int len1 = expendaroundcenter(s, i, i);
int len2 = expendaroundcenter(s, i, i+1);
int temp_len = max(len1, len2);
if(temp_len > max_length) {
max_length = temp_len;
longest_start = i - (temp_len - 1) / 2;
longest_end = i + temp_len / 2;
}
}
return s.substr(longest_start, longest_end - longest_start + 1);
}
int expendaroundcenter(string &s, int left, int right) {
int L=left;
int R=right;
while(L>=0 && R<s.length() && s[R]==s[L]) {
L--;
R++;
}
return R-L-1;
}
};
算法流程
1、对原始字符串进行预处理(添加分隔符)
回文字符串以其长度来分,可以分为奇回文(其长度为奇数)、偶回文(其长度为偶数),一般情况下需要分两种情况来寻找回文,马拉车算法为了简化这一步,对原始字符串进行了处理,在每一个字符的左右两边都加上特殊字符(肯定不存在于原字符串中的字符),让字符串变成一个奇回文,长度由 n n n 变为了 2 n + 1 2n+1 2n+1。例如:
原字符串:abba
,长度为 4
预处理后:#a#b#b#a#
,长度为 9
原字符串:aba
,长度为 3
预处理后:#a#b#a#
,长度为 7
2、计算辅助数组 p p p(回文半径数组)
以字符串 cabbaf
为例,将预处理后的新字符串 #c#a#b#b#a#f#
变成一个字符数组 arr
,定义一个辅助数组 int[] p
,这是一个回文半径数组。
p
的长度与 arr
等长,p[i]
表示以 arr[i]
字符为中心的最长回文半径(这里设定半径不包括中心位置),p[i]=0
表示只有arr[i]
字符本身是回文子串。
i 0 1 2 3 4 5 6 7 8 9 10 11 12
arr[i] # c # a # b # b # a # f #
p[i] 0 1 0 1 0 1 4 1 0 1 0 1 0
我们来比对分下一下最长回文半径和原字符串之间的关系。在上面例子中,最长回文子串是#a#b#b#a#
,它以arr[6]
为中心,半径是4
,其代表的原始字符串是abba
,而abba
的长度为 4,4 就是就是它的长度,是字符串cabbaf
中的最长回文子串,那么我们是不是可以得出最长回文半径和最长回文子串长度之间的关系?
让我们再多看几个例子,如 aba
,转换后是 #a#b#a#
,以字符 b
为中心的回文,半径是 3,3 就是是原字符串的最长回文子串长度。
i 0 1 2 3 4 5 6
arr[i] # a # b # a #
p[i] 0 1 0 3 0 1 0
再例如effe
,转换后是#e#f#f#e#
,以最中间的#
为中心的回文,半径是 4,4 也是原字符串的最长回文子串长度。
i 0 1 2 3 4 5 6 7 8
arr[i] # e # f # f # e #
p[i] 0 1 0 1 4 1 0 1 0
因此,最后我们得到最长回文半径和最长回文子串长度之间的关系:int maxLength = p[i]。maxLength表示最长回文子串长度。
3、计算最长回文子串起始索引
从辅助数组 p p p 中,可以获得最长回文子串的长度,以及其中心在 a r r arr arr 数组中的位置,想要截取出完整的最长回文子串,我们还需要知道它的起始索引值。
原字符串中最长回文子串的起始位置:
s
t
a
r
t
=
(
i
−
p
[
i
]
)
2
start = \frac{(i-p[i])}{2}
start=2(i−p[i])
4、如何高效地计算数组 p p p
如果使用第一节中的中心扩展法来计算回文半径数组 p p p 的话,那么到现在为止,这个做法和“中心扩展法求解最长回文子串”没有什么差别,只不过将原始字符串扩展了一下而已,时间复杂度还是 O ( N 2 ) O(N^2) O(N2)的。
上面的代码不太智能的地方是,对新字符串每一个位置进行中心扩散,会导致原始字符串的每一个字符被访问多次,一个比较极端的情况就是:#a#a#a#a#a#a#a#a#
。事实上,计算机科学家 Manacher 就改进了这种算法,使得在填写新的辅助数组 p 的值的时候,能够参考已经填写过的辅助数组 p 的值,使得新字符串每个字符只访问了一次,整体时间复杂度由
O
(
N
2
)
O(N^2)
O(N2) 改进到
O
(
N
)
O(N)
O(N)。
具体做法是:在遍历的过程中,除了循环变量 i
以外,我们还需要记录两个变量,它们是 maxRight
和 center
,它们分别的含义如下:
maxRight
:记录当前向右扩展的最远边界,即从开始到现在使用“中心扩散法”能得到的回文子串,它能延伸到的最右端的位置 。对于maxRight
我们说明 3 点:- 向右最远”是在计算辅助数组
p
的过程中,向右边扩散能走的索引最大的位置,注意:得到一个maxRight
所对应的回文子串,并不一定是当前得到的“最长回文子串”,很可能的一种情况是,某个回文子串可能比较短,但是它正好在整个字符串比较靠后的位置; - 为什么
maxRight
很重要?因为扫描是从左向右进行的,maxRight
能够提供的信息最多,它是一个重要的分类讨论的标准,因此我们需要一个变量记录它。 maxRight
的下一个位置可能是被程序看到的,停止的原因有 2 点:(1)左边界不能扩散,导致右边界受限制也不能扩散,maxRight 的下一个位置看不到;(2)正是因为看到了maxRight
的下一个位置,导致 maxRight 不能继续扩散。
- 向右最远”是在计算辅助数组
center
:center
是与maxRight
相关的一个变量,它是上述maxRight
的回文中心的索引值。对于center
的说明如下:center
的定义如下:
c e n t e r = a r g m a x { x + p [ x ] ∣ 0 ≤ x ≤ i } center = argmax\{x+p[x] | 0\leq x \leq i\} center=argmax{x+p[x]∣0≤x≤i}
x+p[x]
指的就是我们定义的maxRight
,它取最大值时的x
就是当前center
的值,i
表示的是循环变量,就是从左向右扫描预处理后数组arr
的循环变量。
maxRight
与 center
是一一对应的关系,即一个 cente
r 的值唯一对应了一个 maxRight
的值:
m
a
x
R
i
g
h
t
=
p
[
c
e
n
t
e
r
]
+
c
e
n
t
e
r
maxRight = p[center]+center
maxRight=p[center]+center
因此 maxRight
与 center
必须要同时更新。下面的讨论就根据循环变量 i
与 maxRight
的关系展开讨论:
-
当 i ≥ m a x R i g h t i \geq maxRight i≥maxRight 时,这就是一开始以及刚刚把一个回文子串扫描完的情况,此时只能根据“中心扩散法”一个一个扫描,逐渐扩大
maxRight
; -
当 i < m a x R i g h t i < maxRight i<maxRight 时,根据新字符的回文子串的性质, i i i 关于
center
对称的那个索引(记为mirror
)的 p p p 数组值就很重要。我们先看
mirror
的值是多少,因为center
是中心,i
和mirror
关于center
中心对称,因此(mirror + i) / 2 = center
,所以
m i r r o r = 2 ∗ c e n t e r − i 。 mirror = 2 * center - i。 mirror=2∗center−i。
根据p[mirror]
的数值从小到大,具体可以分为如下 3 种情况:-
p[mirror]
的数值比较小,不超过maxRight - i
:也就是说以
mirror
为中心的最长回文半径不超过maxRight - i
,根据以center
为中心的回文子串的对称性, i i i 为中心的最长回文子串和以mirror
为中心的最长回文子串对称,所以 i i i 为中心的回文子串的最长回文半径与以mirror
为中心的最长回文半径相等,此时,直接把数值抄过来即可,即
p [ i ] = p [ m i r r o r ] 。 p[i] = p[mirror]。 p[i]=p[mirror]。 -
p[mirror]
的数值恰好等于maxRight - i
:仍然是依据以
center
为中心的回文子串的对称性,但是仅能判断p[i]
至少为p[mirror]
,以 i i i 为中心的回文子串还有可能继续扩散,也就会导致maxRight
也更新。因此,可以先把
p[mirror]
的值抄过来(就相当于把maxRight - i
的值抄过来),然后继续“中心扩散法”,继续增加maxRight
。 -
p[mirror]
的数值大于maxRight - i
:以 i i i 为中心的回文子串最长可以扩展到
maxRight
处,所以此时:
p [ i ] = m a x R i g h t − i p[i]=maxRight-i p[i]=maxRight−i -
综合以上三种情况,当
i < maxRight
的时候,p[i]
可以参考p[mirror]
的信息,以maxRight - i
作为参考标准,p[i]
的值应该是保守的,即二者之中较小的那个值:
p [ i ] = m i n ( m a x R i g h t − i , p [ m i r r o r ] ) ; p[i] = min(maxRight - i, p[mirror]); p[i]=min(maxRight−i,p[mirror]);然后再向外扩展。
-