回文子串 Python 普通解 和 Manacher(马拉车) 算法分析

回文子串 Python 一般解 和 Manacher(马拉车) 算法分析

  • 回文就是 abcbaabccba 类型的字符串
  • 题: 求字符串中最长的回文子串
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

  • 一般解 解释:

    1. 特判 return 本身
      1. 如果是单字符 或空
      2. 如果整个都是回文
    2. 初始值:
      1. n 字符串全长
      2. start 最长回文起始点
      3. max_len 回文最长长度
    3. 因为回文性质有分奇偶,所以每次判断都先判断它的两边 再加它的右边 例 回文 abbabb:
      1. 因为a左侧没有字符 所以判断 a加一: ab 因为 ab 不是,所以开始第二步
      2. 判断 b左右加一: abb 不是,再判断 b加一: bb 是,所以 max_len = 2
      3. 判断 bb左右加一: abba 是,所以 max_len = 4
      4. 判断 abba左右加一: left<0 不是,再判断 abba加一: abbab 不是, 所以开始第二步
      5. 判断 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 → \to a 时,两边都为 #(# a #) 所以 a半径 为 1,
    • 当 指针 index = 6 → \to # 时,两边都为 #a#b#c(#a#b#c # c#b#a#) 所以 #半径 为 6,
  • 因为加了 #, 这里的 半径 就是它的回文字符串长度

  • 把所有指针对应的半径值 命名为 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 中心点
      • ) 镜像
        manacher
  • 要点: 当 T < max_right,使用 mirror 可以直接参考左半边的回文 节省计算

    1. 如果 p[mirror] < max_right 的话,直接复制取出 例 P(T_7)
    2. 如果 p[mirror] > max_right 的话,右边继续扩散 例 P(T_9)
    3. 如果 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)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值