简介
这篇文章是关于Manchaer’s Algorithm的学习感想,记录个人的学习成果以及自己的理解。
Mancher’s Algorithm 是用于最长回文子串的求解,具体的背景本小白也不是很清楚,可以去百度或者维基看看。其时间复杂度是O(N),是一个非常简洁的算法。要介绍Manacher’s Algorithm,一定得先介绍回文(Palindrome)。那就从回文开始吧。
回文/Palindrome
回文是正着读过去和反着读过来都一样的字符串,比如“abba” 或者“aba”。回文有三个性质:
- 回文字符串正向和反向是一样的,例如“abcba”
- 回文字符串中,位置关于中心对称的字符是一样的,例如第一个和倒数第一个,第二个和倒数第二个…
- 回文字符串的任意中心相同的子串也是回文,例如“abcba”的子串“bcb”也是回文
其实三点性质的核心就是第二点性质。
相应解法
目前有好多种解法,力扣也有相应的题目。最容易想到的方法便是暴力列举,但其时间复杂度会达到O(N3),消耗太多的时间,并不是一种很好的方法。有地方会提到将最长回文子串转换成最长相同子串的求法,因为回文的反向字符串和本身是一样的。但是这种方法是行不严谨的,例如“accxycca”的逆向字符串是“accyxcca”,用最长相同字符子串来求解的结果是“acc”,很明显这不符合回文特点。除此之外,动态规划(O(N2)),中心扩张(O(N2)),binary search (O(N2logN))都是最长回文子串求解的方法。然而Manacher’s Algorithm / 马拉车算法做到了将时间复杂度进一步降低至O(N)。
算法介绍
预处理
在介绍算法之前,有一个问题需要注意。回文长度有两种可能,奇数“aba”和偶数“abba”。对于奇数长度回文,中心很好找。但偶数长度回文的中心位于两个字符的中间,并不方便查找。为解决这种情况,第一步是对回文进行预处理:在字符之间插入特殊符号,例如“#”或者其他不会出现在字符串中的字符。同时再在最开始插入一个其他字符,用于后续中心和长度的计算,例如“@”。(最后有没有无所谓)
"ababz"->"#a#b#a#b#z#"->"@#a#b#a#b#z#"
回文半径
现在开始介绍Manacher’s Algorithm。在这个算法中,引入了一个和字符串等长的数组,记录以各个字符为中心的回文半径长度。例如“a” 的回文半径是1,“aba”的回文半径是2。
//假设输入字符串为 s
vector<int> p (s.size(),0);
镜像点,中心,右边界
算法会遍历整个字符串,所以有变量 i 表示当前遍历的位置。在此基础上,还增加了三个变量,分别是c, R,mir(当前回文的中心,当前回文的右边界,i 关于中心 c的镜像位置,如图)。现在结合之前的半径数组 p ,来解释设置这些变量的缘由。
算法精髓
为了找出最长回文子串,找到最大的回文半径和相应的位置即可。因此,算法的目的便是完成数组 p 的赋值。在赋值的时候,引入之前设置的三个变量来减少计算量。在具体介绍算法步骤前,需要解释一下算法的精髓:回文半径和镜像点。镜像点的设立是为了更方便快速的计算每一个字符串的回文半径。在暂不考虑镜像点半径超过当前回文的范围的情况下,下图介绍了镜像点的用法。
如图所示,镜像点( mir )的回文长度是3(半径是2),通过回文关于中心点对称的性质,遍历点( i )的回文长度至少是镜像点( mir )的长度。因此,在寻找遍历点的回文半径的时候,就可以从镜像点的长度开始寻找,从而减少了算法的计算量。
"#a# b#b #a#"
^ ^ ^
mir center i
因为在当前回文的外面,可能是个以 i 为中心更长的回文, 所以需要对于遍历点的字符进行新一轮的半径寻找。例如:(位置有限,“#”暂时未被画出)
这种情况下,以 i 为中心的回文半径已经不在当前回文的范围内(i + p[i] > R),意味着以遍历点为中心的回文已经不是以c为中心回文的子串了。当前回文中心点需要更新,以当前遍历点为中心。
对于下一个遍历的字符,可以看作是新回文中的一个点,这也是马拉车算法的更新机制。
之前有一个问题暂时没有考虑,也就是镜像点的半径超过当前回文的左边界的情况,这里讲一下解决方案。举个例子:
图中镜像点回文的长度超过了边界,如果将镜像点半径直接赋值给遍历点的话,遍历点回文最右边的字符“x”是不符合回文的条件的。如若这个字符和最左边的字符相等,当前回文的边界还会往两边扩。正是因为不相等,所以才会有当前格局。
在这种情况下,根据回文的第三个性质,一定能找到镜像点为中心,半径为mirror-L的子回文。因为回文的对称性质,R - i = mirror - L。所以,遍历点的半径在镜像点回文长度超过当前回文边界的情况下,应该被赋值为R - i。这样在设计程序的时候,遍历点的半径应该取镜像点半径p[mir]和 R - i 中小的那一个,即:
p[i] = min(p[mir], R - i);
至此,马拉车算法的基本思路已经讲完了。
代码细节说明
现在假设一个回文“axabaxa”为例子来解释算法过程
第一步,预处理的字符串,得到“@#a#x#a#b#a#x#a#”。
在算法的最开始,先初始化变量。
int mir