算法:Manacher算法求取最大回文子串

算法:Manacher算法求取最大回文子串

九月 06, 2019
当前设备屏幕尺寸过小,推荐使用PC模式浏览。

前导

说起来奇怪,今天早上醒来的时候忽然想到一种根据对称性质求子串的算法。估算了一下复杂度会在线性范围内。

  • 本着我能想到的一定早有人能想到的一贯经历,搜寻了一下Wiki,果然1975年Manacher同志就给出这个算法:

Longest palindromic substring @ wikipedia

不过,也可能是很久之前曾看过这个算法,今早只是突然回想起来了罢。emmm…


算法思路

归一化处理:去除奇偶性

很容易注意到奇数长度的字符串和偶数长度的字符串在对称位上的值是不一样的,如:

序号 0 1 2 3
字母 a b b a
索引 0 1 2
字母 a b a

对称位的序号分别为[1.5][1],为了消除这种奇偶差异,Manacher利用KPM的思想,即首尾及各个值之间添加1个占位符号#,把任意长度的字符串变为奇数长度的字符串。以字符串bccdcf为例,加入占位符#将其去除奇偶性:(索引省略十位上的数字)

索引 0 1 2 3 4 5 6 7 8 9 0 1 2
字母 # b # c # c # d # c # f #

最大回文子串:目标转化

仍旧以字符串bccdcf为例,引入一个新的概念,中心对称序列P

索 引 0 1 2 3 4 5 6 7 8 9 0 1 2
字母 T # b # c # c # d # c # f #
序列 P 0 1 0 1 2 1 0 3 0 1 0 1 0

序列P的值,表示该位(i)上的字母T[i]向左向右P[i]个字母是具备局部最长回文性质的一个子串。例如索引7上的P[7]=3,则T[7-3]~T[7+3]是一个局部最长回文子串。

最后,统计整个序列P中的最大值,就可以找出全局最长回文子串的对称中心所在位置的索引c和左右延展P[c],进而问题得解—— T[c-P[c]] ~ T[c+P[c]] 即为最长回文子串。

例如,上面这个例子中,c=7P[c]=3,最长回文串为"#c#d#c",去除#即为问题的第一个可行解。

  • 综上所述,求解最长回文子串问题的目标转化为了求取序列p的值。

Manacher算法

这个算法的核心思想主要有两个:

  • 对称
  • 扩展

主要利用这两种思想交替求取序列P,进而使问题得解。

序列对称

根据对称性质,我们可以很方便的求得整个序列P

  1. 假设我们有三个指针m(mirror),c(center),i(当前位置),im关于c在[,-\infin,\infin]的整数数轴对称,则根据数学知识显然有 i+m=2ci+m = 2 *c 。(假设字符串数组T的索引值可以从负无穷大到正无穷大)

  2. 根据回文数左右对称的基本性质,若P[c]处是一个极大的值σ\sigma,那么指针c左侧与右侧在极大范围σ\sigma内必然对称。又因为mi关于c对称,则P[i]=P[m],进而求取整个P序列。

这里涉及到两个假设:

  • P[c]的值极大。(该值极大即认为其以c位对称轴成最大回文串) —— 非对称情况
  • 字符串数组T的索引值可以从负无穷大到正无穷大的整数。——边界情况

但在编程中显然不可能有这么便利的情况,因此还需要考虑边界情况非对称情况。在处理这两种情况时,我们采用核心思想的第二种——扩展来求取序列P

序列扩展

事实上,当我们刚拿到一个字符串T时,我们既不能得到cP[c],也不能保证这个字符串中存在回文串(拥有局部对称性)。因此我们首先需要先构建第一个c

  1. 初始化

选择任意字符串T',去除奇偶性之后很容易发现其P序列前两项的值必为[0 1](有些文献将自身长度也记为1,此处为了求索引的方便,不将某个字符认为自身为回文,也即为长度P[0])。

1
2
3
4
5
6
# python3 代码
m_str = placeholder+placeholder.join(st)+placeholder # 去除奇偶影响

p = [0] * len(m_str)
p[1] = 1
c = 1 # center 指针初始化为 1
  1. 扩展以获得序列的下一位

先看下方的算法框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# i 为当前想要求得的P序列索引值
for i in range(2, m_str_len):
m = 2 * c - i # mirror 指针 与 i 关于 center 对称

if 满足扩展条件:
# 进行扩展
else: # 满足对称性质
# 根据对称性质复制回文序列
p[i] = p[m]

# 更新最大回文串的索引值 ( center )
# 取最先找到的一个
if p[i] > p[c]:
c = i

算法的核心就在于如何界定扩展条件/对称条件。

在上一小节中,做出了两种假设情况,不符合这两种情况任一情况的即为满足扩展条件,否则就是对称条件,直接进行p[i] = p[m]的处理即可:

  • 非对称情况:以c为中心的P[c]无法保证在P[c]覆盖范围im保证对称性。
  • 边界情况:mirror 越界,在编程中就是通过m = 2*c-i 求算m时,m < 0的情况。

边界情况相对容易理解,第一种情况(非对称情况)该如何理解呢?

1.满足对称时:
满足对称
上图中,mci为当前所求索引 i 关于最大中心对称点 c 的对称点 m 且满足m0m \geq 0
黑色方框部分为以c为中心的P[c]的覆盖范围,我们自左向右求取P[...]的值,则P[m]已知且左侧黄框部分为以m为中心的P[m]覆盖范围。根据对称性可知,此时P[i]=P[[m]

  • 此时满足对称条件

2.边界相切时
边界相切时
当我们继续向右移动i,相同的m会向左移动,若此时左侧覆盖范围刚好与P[c]的覆盖范围相切,则根据对称性,右侧也应该恰好相切。

  • 不同的是,由于我们自左向右求,因此左侧的所有情况都是已知的,而右侧却是未知的。
  • 在右侧相切时,可能P[i]覆盖范围的左侧和右侧(右侧是P[c]-P[m]个未被扫描过的元素),而此时恰好满足二者相等,即满足对称性,则P[i]=P[m]+xx扩展过程中发现的关于i为对称轴的回文元素。扩展时从P[m]起进行左右扫描匹配即可。
  • 此时满足扩展条件

3.左侧溢出情况
左侧溢出情况
i继续向右,若此时P[m]的覆盖范围超出了P[c]的覆盖范围,根据以c为对称中心的回文串的对称性质,并不能保证右侧越出红线的若干个值是以i为对称中心的回文串,因此需要以i为中心,从0起进行扩展。

  • 此时满足扩展条件

概括上面三种情况,后两种满足扩展条件,因此代码为:

1
2
3
4
5
6
7
8
9
10
11
12
if m < 0 or m-p[m] <= c-p[c]: # 满足扩展性质
# 最大扩展半径
r = min(i, m_str_len-i-1) # 防止数组越界
# 最大已知回文半径
p[i] = 0
for j in range(p[i], r):
if m_str[i - j - 1] == m_str[i + j + 1]: # 左右扫描进行匹配
p[i] += 1 # 若匹配成功,对P[i]进行自增(P[i]初始值为0)
else:
break
else: # 满足对称性质
p[i] = p[m] # 根据对称性质复制回文序列

Tips:注意到上述后两种情况,已知的最大回文半径P[i]可以进一步设为 p[i] = max(0, c + p[c] - i),以加速算法匹配速度。

完整的实现参考下方的python代码,结合代码可以更好的理解。
虽然和Wiki上的Manacher算法的解释不一致,但是本质是一样的。


python代码

  • 代码去除注释,核心部分只有10行左右,很容易读懂
    manacher @ 码云
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def manacher(st, placeholder="#"):
# 去除奇偶影响
m_str = placeholder+placeholder.join(st)+placeholder
# 初始化回文序列 p=[0 1 0 0 ...]
p = [0] * len(m_str)
p[1] = 1

c = 1 # center初始化为1
m_str_len = len(m_str)
for i in range(2, m_str_len):
m = 2 * c - i # mirror 与 i 关于 center 对称
# 判断p[m]与p[i]是否一致(m的左侧覆盖超出了c的左侧覆盖)
# 若不一致或mirror越界(m<0)则进行扩展
# 注意:python中负索引是合理索引值,其它语言应自行考虑m的越界情况
if m < 0 or m-p[m] <= c-p[c]:
r = min(i, m_str_len-i-1) # 最大扩展半径
p[i] = max(0, c + p[c] - i) # 最大已知回文半径
for j in range(p[i], r, 1):
# 进行扩展
if m_str[i - j - 1] == m_str[i + j + 1]:
p[i] += 1
else:
break
else:
# 根据对称性质复制回文序列
p[i] = p[m]

# 更新最大回文串的索引值
# 取最先找到的一个
if p[i] > p[c]:
c = i

return m_str[c-p[c]:c+p[c]+1].replace(placeholder, "")

> 点此下载代码