这个算法是在做一道“求最长回文子串”的题目时查到的一个算法,在此之前的方法是遍历所给字符串,使用中心展开的方法求的最长的子串
def
上面的中展开求长度的用时,加上遍历字符串的循环,时间复杂度为
这个算法最大的一个问题就是重复计算,没有充分利用回文字符串的特性——对称性,所以改进的关键就在于减少这种不必要的展开,充分利用对称性,不多说了,先上栗子
现给定一个字符串T:#b#a#b#c#b#a#b#c#b#a#x
为什么会有这么多‘#’,后面会解释,这里直接看做字符串本身的一部分,另外最后那个x并不是字母‘x’,而是指代未知字符,后面会详细说明
另外在设置一个与字符串等长的数组p,存储字符串中对应字符的“对称半径(不包含自身)”,例如p[0]=0,p[1]=1,p[2]=0,所以将数组p中所有的元素都算出来后,自然就得到最大回文子串的信息了,计算方法如下图:
上图中第一排是索引,从0开始,第二排是就是字符串T对应索引的字符(废话),第三排是该字符的对称半径
假设现在对计算到p[11] = 9了,这个是已知的,设该点为c,即c=11,也就是上图中标黄的a的位置,上图标绿的就是它的对称半径的边界
另外,标红的就是之前T里面的未知字符x,对算法而言,现在除了知道它不等于‘b’(等于b的话p[11]就不是9了)之外其余一概不知,再往后的字符就更不知道了,所以是‘?’表示
接下来,就是要进一步计算p中剩下的值了,现在轮到p[12]了,首先,找到i=12关于c的对称点,即10,设为j,关系如下
j = 2 * c - i
然后检查p[j](即p[10])的对称半径是否在T[c](标黄的a)的对称半径范围之内,观察上图可知在c的覆盖范围之内,因为p[j] = 0,所以p[i]也等于0
理解这一点很关键,因为以a为中心的左右9个字符都是对称的,所以p[i] = p[j],如果不等于,就破坏了T[c]的对称性,如果还是不理解可以再看一个例子
i=13,所以j=9,p[j]=1,其覆盖范围如图中灰色部分,同样没有超过c对称半径的边界(标绿的),所以p[i] = p[j] = 1,为什么?因为以c为中心,左边9个字符和右边9个字符是对称的,所以他们的对称性(在不超过c的边界之前)是一样的
if
上面的mx表示c的对称边界,即mx = c + p[c]
根据同样的方法可以轻松计算出p[14] = 0,接下来是p[15],如图:
可以看到j = 7, p[j] = 7,其对称范围已经超过c的对称边界了,因为c的对称性约束,可以知道p[i] = mx - i = 5
可能有人会有疑问,为什么是5,不是7?可以看到,
但是这样一来,p[c]就不等于9了,想不明白的可以往上看看
if
再来看另外一个栗子:
此栗中,c=5,i=7,j=3,可以看到,p[j]=1,其对称半径正好卡在c的边界上,这时候p[i]等于多少?只能说至少等于1,再上一个图说明原因:
可以看到,该图和之前那个图并不矛盾,之前T[9]只检查到不等于d(因为p[c]=3的约束),但是可以等于其他的字符啊,例如‘c’,包括后面的字符,也是未知的,需要进一步去判断,所以,这种情况除了使用中心展开法挨个对比就没有更好的办法了
j
上面的max_id和max_radius是记录的最大对称半径和其索引
上面的算法,充分的利用了回文字符串的对称特性,避免了重复计算,其实上面的算法也就是马拉车算法的主要步骤,当然,还有一种情况没有说明,比如i > mx的情况(例如刚开始对字符串进行遍历时),这种情况也是直接用中心展开求得半径
还有就是上面说的‘#’字符问题,这个是为了避免因为所给字符串的长度不同(奇数或偶数),统一在字符串各字符之间以及收尾插入一个‘#’号,这样无论所给字符串是奇数个还是偶数个字符,都能调整为奇数
下面,贴上完整代码:
def