文章目录
- 回文子串 Python 一般解 和 Manacher(马拉车) 算法分析
- 普通解
- Manacher(马拉车) 算法
回文子串 Python 一般解 和 Manacher(马拉车) 算法分析
- 回文就是
abcba
或abccba
类型的字符串 - 题: 求字符串中最长的回文子串
answer = 'abc'*5600+'cedec'*5706 + 'cba'*5600 # 最长回文
question = 'qwesc'*1035 + answer + 'qwversaqe'*1204 # 问题字符串
普通解
- 首先,一般的想法就是 从头到尾,依次选取进行比较。
- 因为,只需要求最长的,设定一个
max_len
作为门宽,可以写得一般的解:
class BasicSolution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2 or s == s[::-1]:# 特判
return s
start, max_len = 0, 1
for i in range(1,n):
left = i-max_len
if left-1>=0 and s[left-1:i+1] == s[i-n:left-n-2:-1]: # 加二
start = left-1 # 因为加二 所以start 要退一格
max_len += 2
elif left>=0 and s[left:i+1] == s[i-n:left-n-1:-1]: # 加一
start = left
max_len += 1
return s[start:start+max_len]
sol = BasicSolution()
%timeit sol.longestPalindrome(question)==answer
8.65 s ± 74.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
True
计算时间: 8.65 s, 内存占用: 0.4 MiB
- 一般解 解释:
- 特判
return
本身
- 如果是单字符 或空
- 如果整个都是回文
- 初始值:
-
n
字符串全长 -
start
最长回文起始点 -
max_len
回文最长长度
- 因为回文性质有分奇偶,所以每次判断都先判断它的两边 再加它的右边 例 回文
abbabb
:
- 因为a左侧没有字符 所以判断
a加一: ab
因为ab
不是,所以开始第二步 - 判断
b左右加一: abb
不是,再判断b加一: bb
是,所以max_len = 2
- 判断
bb左右加一: abba
是,所以max_len = 4
- 判断
abba左右加一: left<0
不是,再判断abba加一: abbab
不是, 所以开始第二步 - 判断
bbab左右加一: abbabb
不是,再判断bbab加一: bbabb
是,max_len = 5
- 这个解法看似是 O(n), 其实里面 判断两个字符串是否回文 依赖于python 的字符串对比, 所以它实际上并不属于 O(n)。
- 接下来介绍一个 Manacher 的算法。因为读音近似于中文 马拉车, 所以一般有人称它为马拉车算法。
Manacher(马拉车) 算法
- 由科学家 Manacher 研究的算法。
- 在字符串中插入
#
使得字符串变成#a#b#c#b#a#
或#a#b#c#c#b#a#
使得更利于表示回文 半径
string = # a # b # c # c # b # c # c #
indexs = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
- 当 指针
index = 1
a
时,两边都为#
,(# a #)
所以a
的 半径 - 当 指针
index = 6
#
时,两边都为#a#b#c
,(#a#b#c # c#b#a#)
所以#
的 半径
- 因为加了
#
, 这里的 半径 - 把所有指针对应的半径值 命名为
p
string = # a # b # c # c # b # c # c #
indexs = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
p = 0 1 0 1 0 1 4 1 0 5 0 1 1 1 0
- Manacher 用的是中心扩散法:
- 符号意义:
-
*
未知值 -
T_1
时间步骤 -
!
max_right 指针,搜索到的位置 -
|
center 中心点 )
镜像
- 要点: 当
T < max_right
,使用mirror
可以直接参考左半边的回文 节省计算
- 如果
p[mirror] < max_right
的话,直接复制取出 例P(T_7)
- 如果
p[mirror] > max_right
的话,右边继续扩散 例P(T_9)
- 如果
max_right
到尽头了, 取max_right -T
和 第一个步骤取最小值 例P(T_10)
# Manacher 算法
class ManacherSolution:
def longestPalindrome(self, s: str)-> str:
if len(s) < 2 or s == s[::-1]:# 特判
return s
string = '#'+'#'.join(s)+'#' # 预处理字符串
n = len(string)
p = [0 for _ in range(n)] # 初始化 p
max_right, center = 0,0 # 对应的双指针,须同时更新
start, max_len = 1,1 # 当前遍历的中心最大扩散步数 和 起始位置,须同时更新
for i in range(n): # i -> index
if i < max_right:
mirror = 2*center -i
p[i] = min(max_right -i, p[mirror])
left, right = i -(1+p[i]), i+(1+p[i]) # 扩散的左右指针
# left >= 0 and right < n 保证不越界
# t[left] == t[right] 表示可以再扩散 1 次
while left >=0 and right< n and string[left]==string[right]:
p[i] += 1
left -= 1
right += 1
# 扩散后 找到 p[i]
# max_right 为 p[i]+ i 就是图上的 !标志
# i < max_right 使得 可以重复利用回文信息
if i + p[i] > max_right:
max_right, center = i+p[i], i # max_right 和 center 需要同时更新
if p[i] > max_len:
max_len = p[i]
start = (i - max_len) // 2 # 因为 扩大了两倍
return s[start: start + max_len]
sol = ManacherSolution()
%timeit sol.longestPalindrome(question)==answer
345 ms ± 7.94 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
True
计算时间: 345 ms, 内存占用: 2.7 MiB
- 因为构建了个 P 所有比之前的更占用内存。
- 但是计算空间 优化到 O(n)