原博主已经写得非常清楚了,这里我只是将原文转过来,原博文如下:Manacher算法总结
Manacher算法介绍
Manacher 算法是查找一个字符串的最长回文子串的线性算法。在介绍算法之前,首先介绍一下什么是回文串,所谓回文串,简单来说就是正着读和反着读都是一样的字符串,比如 abba,noon 等等,一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。
计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为
O(n3)
O
(
n
3
)
的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为
O(n2)
O
(
n
2
)
,但是当n比较大的时候仍然无法令人满意,Manacher 算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。
Manacher算法原理与实现
下面介绍Manacher算法的原理与步骤。
首先,Manacher 算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用 # 号。下面举一个例子:
(1) Len 数组简介与性质
Manacher 算法用一个辅助数组
Len[i]
L
e
n
[
i
]
表示以字符
T[i]
T
[
i
]
为中心的最长回文字串的最右字符到
T[i]
T
[
i
]
的长度,比如以
T[i]
T
[
i
]
为中心的最长回文字串是
T[l,r]
T
[
l
,
r
]
,那么
Len[i]=r−i+1
L
e
n
[
i
]
=
r
−
i
+
1
。
对于上面的例子,可以得出
Len[i]
L
e
n
[
i
]
数组为:
Len
L
e
n
数组有一个性质,那就是
Len[i]−1
L
e
n
[
i
]
−
1
就是该回文子串在原字符串
S
S
中的长度,至于证明,首先在转换得到的字符串 中,所有的回文字串的长度都为奇数,那么对于以
T[i]
T
[
i
]
为中心的最长回文字串,其长度就为
2∗Len[i]−1
2
∗
L
e
n
[
i
]
−
1
,经过观察可知,
T
T
中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有 个分隔符,剩下
Len[i]−1
L
e
n
[
i
]
−
1
个字符来自原字符串,所以该回文串在原字符串中的长度就为
Len[i]−1
L
e
n
[
i
]
−
1
。
有了这个性质,那么原问题就转化为求所有的
Len[i]
L
e
n
[
i
]
。下面介绍如何在线性时间复杂度内求出所有的
Len
L
e
n
。
(2) Len数组的计算
首先从左往右依次计算
Len[i]
L
e
n
[
i
]
,当计算
Len[i]
L
e
n
[
i
]
时,
Len[j](0⩽j<i)
L
e
n
[
j
]
(
0
⩽
j
<
i
)
已经计算完毕。设
P
P
为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为 ,分两种情况:
第一种情况:
i⩽P
i
⩽
P
那么找到
i
i
相对于 的对称位置,设为
j
j
,那么如果 ,如下图:
那么说明以
j
j
为中心的回文串一定在以 为中心的回文串的内部,且
j
j
和 关于位置
po
p
o
对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即
Len[i]⩾Len[j]
L
e
n
[
i
]
⩾
L
e
n
[
j
]
。因为
Len[j]<P−i
L
e
n
[
j
]
<
P
−
i
,所以说
i+Len[j]<P
i
+
L
e
n
[
j
]
<
P
。由对称性可知
Len[i]=Len[j]
L
e
n
[
i
]
=
L
e
n
[
j
]
。
如果
Len[j]⩾P−i
L
e
n
[
j
]
⩾
P
−
i
,由对称性,说明以
i
i
为中心的回文串可能会延伸到 之外,而大于
P
P
的部分我们还没有进行匹配,所以要从 位置开始一个一个进行匹配,直到发生失配,从而更新
P
P
和对应的 以及
Len[i]
L
e
n
[
i
]
。
第二种情况:
i>P
i
>
P
如果
i
i
比 还要大,说明对于中点为
i
i
的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的 以及
Len[i]
L
e
n
[
i
]
。
Python 实现如下,虽然这里也写了预处理函数,但是其实在 Python 中一句话就可以实现预处理函数,因此这里的预处理函数没用到。由于对转换后字符串的每个字符只遍历了一次,因此时间复杂度是线性的:
def pre_process(s):
n = len(s)
if n == 0: return "^$"
t = "^"
for i in range(0, n, 1):
t += '#' + s[i]
t += '#$'
return t
def mancher(s):
# p[i] records the longest palindrome substring centered on s_new[i]
# ps and pl is the longest palindrome substring and its length
# r is the maximum value of the right end point of the longest palindrome substring in the previous calculation
# and the position obtained from this maximum is pos
# s_new = pre_process(s)
s_new = "^$" if len(s) == 0 else '^#' + '#'.join(s) + '#$'
p = [0] * len(s_new)
pl = pos = r = 0
ps = ""
for i in range(1, len(s_new) - 1, 1):
# 2 * pos - i is i's symmetry point about pos
p[i] = min(r - i, p[2 * pos - i]) if r > i else 1
while s_new[i - p[i]] == s_new[i + p[i]]:
p[i] += 1
if p[i] + i > r:
r = p[i] + i
pos = i
# update
if pl < p[i]:
pl = p[i] - 1
begin = (i - p[i]) / 2
ps = s[begin:begin + pl]
return pl, ps