5. 最长回文子串
1. 简介
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
1 | 输入:s = "babad" |
示例 2:
1 | 输入:s = "cbbd" |
示例 3:
1 | 输入:s = "a" |
示例 4:
1 | 输入:s = "ac" |
提示:
1 <= s.length <= 1000s仅由数字和英文字母(大写和/或小写)组成
2. 题解
2.1. 动态规划
如果字符串 s 是回文串,并且长度大于等于 3,那么字符串 s 去除首尾两个字符的子串也是回文串
长度为 1 的字符串一定是回文串
以 "ababa" 为例1,如果已知 "bab" 是字符串是回文串,因为 "ababa" 首尾是相同字符 "a",那么 "ababa" 也是回文串。
设 $P(i, j)$ 表示 s 的子串 s[i, j] 是否为回文串,即
s[i, j]表示s的第i到第j个字符组成的字符串,可取到右边界
其他情况包括以下两种可能性:
s[i, j]不是回文串;i>j。
根据上述的公式,可得动态规划的状态转移方程:
即:子串为回文串 且 当前字符串的首尾字符相同
初始状态:
- 字符串长度为 1,一定是回文串,即 $P(i,i)=True$;
- 字符串长度为 2,如果字符相同则为回文串,即 $P(i,i+1)=(s[i]==s[j])$ 。
我们用图片来展示动态规划的流程:
初始化 dp,首先
dp[i][i]=True,表示字符串长度为 1 的一定是回文子串,如果s[i]==s[i+1],则dp[i][i+1]=True,否则dp[i][i+1]=False。在初始化的过程中,记录最长回文子串的起始位置和对应的长度。
初始化时把长度为 1 和长度为 2 的子串遍历完了,下面直接从长度为 3 的子串开始遍历。
首先是
s[0, 2],因为s[1, 1]^(s[i]==s[j]) = True,此时更新最长回文子串的起始位置为 0,长度为 3。
遍历下一个长度为 3 的子串中,此时的子串为
s[1, 3],有s[2, 2]^(s[1]==s[3]) = True,这个回文子串没有大于 3,可以不用更新最长回文子串。
全部子串(包括长度大于3的子串)遍历完成后得到的上图结果,返回最长回文子串即可。
根据上述过程,实现的 python 代码如下
- 时间复杂度:$O(N^2)$
- 空间复杂度:$O(N^2)$
1 | class Solution: |
2.2. 中心扩展
观察状态转移方程
找出其中的状态转移链
可以发现,所有状态在转移的时候可能性都是唯一的1,如果我们从每一种边界情况开始扩展,就可以得到所有状态对应的答案。
边界子串长度为 1 的情况
子串长度为 2 的边界情况
边界即子串长度为 1 或 2,如果两边的字母相同,我们就可以继续扩展,从 $P(i+1,j-1)$ 扩展到 $P(i,j)$,如果两边的字母不同,说明这之后的子串都不是回文串,停止扩展。实现的 python 代码如下
- 时间复杂度:$O(N^2)$
- 空间复杂度:$O(1)$
1 | class Solution: |
2.3. Manacher算法
Manacher 算法(马拉车算法)本质上还是用中心扩展的思想2,回文串可分为奇数回文串和偶数回文串,它们的区别是:奇数回文串关于它的“中点”满足“中心对称”,偶数回文串关于它“中间的两个点”满足“中心对称”。
为了消除字符串长度是奇数和偶数带来的差异,首先需要对字符串进行预处理,也就是添加分隔符:在字符串的首尾、相邻的字符中插入分隔符,例如 "baba" 添加分隔符 # 以后得到 "#b#a#b#a#" 。
插入分隔符后,不管原字符串长度是奇数还是偶数,最后得到的都是奇数长度的字符串。
我们计算每个位置 i 开始向外扩展的最大回文子串长度,在此过程中记录最大的回文子串长度即可。在计算以 i+1 为中心的回文子串长度时,我们可以利用位置 i 上的信息。分 2 种情况讨论:
r表示当前位置i向外扩展的最大右边界,数组P来记录每个位置向右扩展的最大长度。
c表示目前找到的最长回文子串的中心位置,并用max_len记录最大回文子串长度。
假设我们目前找到的最长子串中心位置为 c=6 ,该位置的最大扩展右边界为 r=10,对于以 c 为中心的任何一个回文字符串来说,r 关于 c 的镜像的索引是 l = 2*c - r 。Manacher 的核心就是借助 c、r、l 提供的信息,在求 P[i] 的时候,不用暴力地向两侧扩展,易知以位置 i 为中心的回文字符串的长度为 2*P[i]+1。
(一)第一种情况,镜像索引向左扩展的步长没有超过已知最大回文子串的左边界,那么 i 处至少可以向外扩展 P[i'] 步
如图所示,此时 i < r,我们不用直接从 i=7 处向外扩展,可以先计算以 i=7 为中心的最短回文串长度,在此基础上得到 P[i]。
先计算 i 的关于 c 的镜像索引 i' = 2*c -i,如果 i' - P[i'] 没有小于 l ,即从位置 i' 向外扩展 p[i'] 步后,没有超出已知最大回文子串的左边界,那么位置 i 至少可以向外扩展 P[i'] 步。
这里利用的是以
c为中心的回文字符串的对称性,只要没有超过这个回文字符串的边界,从i往右、左走多少步,和从i'往左、右走多少步所看到的字符时一样的,所以在此条件下i'能往左走多少步,i就能往右走多少步。
位置 i 实际可以向外扩展的长度可能会更大,所以还需要在此基础上继续扩展。
(二)第二种情况,即 i' - P[i'] 小于 l,那么 i 处至少可以向外扩展 r-i 步
如图所示,此时 i'-P[i']=0 ,而 l=2 ,即从位置 i' 向外扩展 P[i'] 步后,超出已知最大回文子串的左边界,所以 P[9] 至少为 r - i = 3 。
即以
i为中心的最小回文字符串,向右扩展的长度不超过已知最大回文子串的右边界r,图中例子r刚好在边界处。如果
r不是边界,假设上述字符串s长度不止 13,且以i=9为中心的回文字符串,有s[13]=s[5]='a',此时会有s[1]=s[13]='a',那么以i=7为中心的最长回文子串长度就变长了,不止是 5;如果s[13]≠'a',在此情况下刚好向右扩展r-i=12-9=3步。
上面两种情况满足公式:
然后从 i 左右 P[i] 的位置处继续向两侧扩展即可,如果以 i 为中心的回文字符串的右边界超过了 r,则更新 c 为i ,更新 r 为 i + P[i]。实现的 python 代码如下3:
- 时间复杂度:$O(N)$
- 空间复杂度:$O(N)$
1 | class Solution: |
参考资料
1. https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/ ↩