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 <= 1000
s
仅由数字和英文字母(大写和/或小写)组成
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/ ↩