Manacher's algorithm 马拉车算法

_

参考

问题描述

求字符串的子串{回文串}{最长}

denote

这里均用Zero-based index

s

原字符串

n

len(s)

S

变换后的字符串

ns

len(S)

P

P[i]表示以S[i]为中心的最长回文子串的长度(相对于原字符串s而言)

expand_around_center

采用的是leetcode solution里面的说法, 大意是所有回文串的中心只可能有2n-1, 考虑每个位置作为中心的情况, 从中心往两侧扩宽回文串的长度

Prerequisite

python3

expand_around_center

def longestPalindrome(self, s: str) -> str:

n=len(s)

ans=0

for i in range(n):

l = 0

while 0<=i-l<n and 0<=i+l<n and s[i-l]==s[i+l]: l+=1

ans = max(ans, l*2+1)

for i in range(n-1):

l = 0

while 0<=i-l<n and 0<=i+l+1<n and s[i-l]==s[i+1+l]: l+=1

ans = max(ans, l*2)

retur ans

思路

首先在原字符串每个字符前后插入某个字符,比如#

巧妙的不再需要讨论奇偶回文串, 化虚(间隙)为实(字符'#')

#为中心是偶回文串(abba)

否则是奇回文串

也可以在最开始最末尾补上用来终止的两种字符,比如$^, 作为哨兵, 这样就不用判断边界条件了

   

不难发现P[i]也是以字符S[i]为中心的 最长回文子串 左边部分的长度(不包括S[i])

   

也可以不进行变换, 变换是为了便于理解和编程, 不是必须的

   

相比于找到2n-1的可能的回文串中心位置的算法expand_around_center, manacher算法就做了一处优化

对于这处优化的示例

假设我们用expand_around_center算法, 正在从左往右计算出S各个位置的最长回文子串的长度, 其中P[8]还没有计算

expand_around_center算法没有必要存下各个位置最长回文子串的长度, 这里是为了举例

当判断到S[8], 已经知道了P[7]=6,

那么S[1:7]S[8:14]是以S[7]为中心左右对称的,

又因为P[6]=1, 说明S[4]!=S[8]; S[5]==S[7]

也就是说S[6]!=S[10]; S[7]==S[9]

所以P[8]=1

这种通过在S[7]前面的P[6]来判断P[8]的方法只需要满足一个前提:

8+P[6]<7+P[7]

如果不满足前提, S[12]为例,

12+P[2]>=7+P[7]

此时P[12]>=P[2], 那么还需要判断S[12+P[2]+1]S[12-P[2]-1], 以及离S[12]更远的字符

所以存在一种方法能够由S[7-k]推测S[7+k], 7+P[7]相当于一个"最远边界", 边界内的字符存在对称性, 可以跳过部分字符的比较过程, 边界外的则用普通的方法比较(参见expand_around_center)

把这种优化方法补充进expand_around_center中就成了manacher算法

算法的细节部分是, 需要维护一个P数组, 同时可以采用维护一个最远边界{当前所有算出了P的字符的"最远边界"}的方法

centerRight := max(t+P[t])

在代码中把最远边界命名为centerRight, 最远边界对应的对称中心处的字符的索引命名为centerIdx

   

代码

绿色字体的部分代码, 不影响得到P的整个算法, 是一个获取答案的切面

def longestPalindrome(self, s: str) -> str:

"""

get length of longest palindrome substring of s

"""

n=len(s)

S_bui = []

S_bui.append('^')

S_bui.append('#')

for i in range(n):

S_bui.append(s[i])

S_bui.append('#')

S_bui.append('$')

S="".join(S_bui)

ns=len(S)

longestPal=slice(0,0)

centerRight=0

centerIdx=0

P=[0]*(ns)

for i in range(2,ns-2):

if centerRight>i:

if P[2*centerIdx-i]+i<centerRight:

P[i]=P[2*centerIdx-i]

continue

else:

P[i]=centerRight-i

# else P[i]=0

while S[i+P[i]+1]==S[i-P[i]-1]: P[i]+=1

   

if i+P[i]>centerRight:

centerRight=i+P[i]

centerIdx=i

if longestPal.end-longestPal.start<P[i]:

longestPal=slice(i-P[i], i+P[i]+1)

return S[longestPal].replace('#','')

   

时间复杂度

O(n)

上面的代码由一个for循环, 和里面一个while循环 组成

   

如果while内进行了k次循环, 新的centerRight 会变成 centerRight+k 或者 k+i(此时i>=centerRight), centerRight上限是ns-2, 因此所有的while内循环一共最多发生O(ns)

   

综上时间复杂度是O(n)

   

不进行变换的代码(main)

需要分回文串长度的奇偶进行讨论, 但是速度快很多, 代码基本不用变

下面是一个示例

当参数odd=1, P[i]保存了以字符S[i]为中心的 (奇数长度)最长回文子串 左边部分的长度(不包括S[i])

当参数odd=0, P[i]保存了以字符S[i]右侧间隙为中心的 (偶数长度)最长回文子串 左边部分的长度(包括S[i])

def longestPalindrome(s, odd):

"""

odd is 0 or 1

get longest palindrom substring whose length%2==odd

"""

n=len(s)

P=[0]*n

centerRight=0

centerIdx=0

longestPal=slice(0,0)

for i in range(n):

if centerRight>i:

if P[2*centerIdx-i]+i<centerRight:

P[i]=P[2*centerIdx-i]

continue

else:

P[i]=centerRight-i

while i-P[i]-odd>=0 and i+P[i]+1<n and s[i+P[i]+1]==s[i-P[i]-odd]:

P[i]+=1

if i+P[i]>centerRight:

centerRight=i+P[i]

centerIdx=i

   

if longestPal.end-longestPal.start<P[i]*2+odd:

longestPal=slice(i-P[i]+1-odd:i+P[i]+1)

return s[longestPal]

posted @ 2020-10-20 17:21  migeater  阅读(155)  评论(0编辑  收藏  举报