[Leetcode 5] 最长回文子串

type
status
date
summary
tags
category
icon
password
featured
freq
difficulty
找字符串 s 中最长的回文子串。
 
示例 1:
示例 2:
 
 

解法一:中心扩展(推荐)

notion image
以字符串中的每个位置(可以是单个字符,也可以是两个相邻字符之间的空隙)作为回文中心。
  • 奇数回文中心在字符上(如 "aba"b);
  • 偶数回文中心在两字符之间(如 "abba"bb 之间)。
从中心向两边同时扩展,尝试找到以该中心为对称轴的最长回文子串。
由于回文串的左右对称的特性,只要两边字符相等,就持续扩展,直到两边字符不相等或超出边界。
枚举所有可能的中心,分别计算其对应的最大回文长度,最长的那个子串作为答案。
复杂度
  • 时间:O(n^2)(中心数是 2n-1,最坏扩展到两端)
  • 空间:O(1)

解法二:二维动态规划(区间 DP)

如果子串 s[i+1..j-1] 是回文,并且两头字符 s[i] == s[j],那么把这两个相等的字符包上去,得到s[i..j] 也是回文。
dp[i][j] 表示子串 s[i..j] 是否是回文。
  • Base Case:长度 1 的串天然是回文。
  • 计算 dp[i][j]用到 dp[i+1][j-1] 的结果,就必须从短到长地枚举区间。也就是算长度为 L 的区间,先要把长度为 L-2 的区间算完。
    • 所以我们就从 L = 1 开始递增,枚举从索引i 开始的长度为 L 的子串 ( j = i + L - 1)。
      由于 dp[i+1][j-1] 的长度是 L-2,在计算长度为 Ldp[i][j]之前已经先计算过了,可以直接用。
复杂度
  • 时间:O(n^2)(所有区间各看一次)
  • 空间:O(n^2)(完整 DP 表)

解法三:一维动态规划

二维 DP在内存中有整个 n^2 的网格,实际上不是必需的。
注意到 每次我们算一个值dp[i][j] 时,都只需要 查看它斜对角dp[i+1][j-1] 的值。
任何时候,我们只需要缓存前一行的值。然后原地修改。
当固定 r=j 时,只需要用一维数组 dp[l] 表示 s[l..r] 是否回文。
因为在二维 DP 中dp[i][j] 依赖 dp[i+1][j-1] , 所以让 i 从右往左扫i = j, j-1, ..., 0)更新 dp[l]
再用一个变量 prev 暂存上一轮(r = j -1)的 dp[l+1],这个值就是二维DP表里斜对角dp[l+1][r-1]
这样只用 一行数组 + 一个常数变量 prev 就能表示二维 DP 表,从而把空间从 O(n^2) 降到 O(n)
复杂度
  • 时间:O(n^2)
  • 空间:O(n)(一行 DP数组)

解法四:Manacher (最优解)

  1. 先在字符串里插分隔符,比如 "abba" 变成 #a#b#b#a#。这样所有回文都变成“奇数长度”, 这样只需要维护一种情况。
  1. 在我们扫描到当前位置之前,所有已经找到的回文子串里,那个右端点最靠右的回文的中心是 center,右边界是 right
    1. 当我们来到新位置 i
      • 如果 i 还在这段“最右回文”的覆盖里(i < right),那么 i 关于 center对称点 mirror = 2*center - i 的半径 p[mirror],可以直接给 p[i] 一个下界(最少这么长)。更具体地说,p[i] 至少是 min( right - i, p[mirror] )
        • 这一步就像“白嫖”一段已经核实过的对称信息,不用从 0 开始扩
      • 然后再在这个下界基础上,向外微调扩展(匹配不上才停)。
      • 如果 i + p[i] 超过了之前的 right,那就更新“最右回文”的 center = iright = i + p[i]
      • 同时记录全局最大的 p[i] 和它的中心,最终把答案从“带 # 的串”映射回原下标。
复杂度
  • 时间O(n)(右边界单调右移,总扩展次数线性)
  • 空间O(n)(预处理后的字符串与半径数组)
 
上一篇
[Leetcode 4] 两个排序数组的中位数
下一篇
[Leetcode 11] 盛最多水的容器