5. 最长回文子串
Question
给你一个字符串 s,找到 s 中最长的回文子串。
Example 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。的和最大,为 6 。
Example 2:
输入:s = "cbbd" 输出:"bb"
Example 3:
输入:s = "ac" 输出:"a"
Note:
- 1 <= s.length <= 1000
- s 仅由数字和英文字母(大写和/或小写)组成 <= 105`
Approach 1: 动态规划
对于一个子串而言,如果它是回文串,并且长度大于
2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串
根据这样的思路,我们就可以用动态规划的方法解决本题。我们用
s[i, j] 本身不是一个回文串;
i > j,此时 s[i, j] 本身不合法。
那么我们就可以写出动态规划的状态转移方程:
上文的所有讨论是建立在子串长度大于 2 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 1 或 2。对于长度为 1 的子串,它显然是个回文串;对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
1 | class Solution: |
复杂度分析
- 时间复杂度:
,其中 是字符串的长度。动态规划的状态总数为 ,对于每个状态,我们需要转移的时间为 。 - 空间复杂度:
,即存储动态规划状态需要的空间。
Approach 2: 中心扩展算法
我们仔细观察一下方法一中的状态转移方程:
边界情况即为子串长度为 1 或 2 的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。如果两边的字母相同,我们就可以继续扩展,例如从 P(i+1,j-1) 扩展到 P(i,j);如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。
聪明的读者此时应该可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。方法二的本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。
1 | class Solution: |
复杂度分析
复杂度分析
时间复杂度:
,其中 n 是字符串的长度。长度为 1 和 2 的回文中心分别有 n 和 n-1 个,每个回文中心最多会向外扩展 次。空间复杂度:
。
Approach 3: Manacher 算法
还有一个复杂度为
为了表述方便,我们定义一个新概念臂长,表示中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * length + 1 ,其臂长为 length。
下面的讨论只涉及长度为奇数的回文字符串。长度为偶数的回文字符串我们将会在最后与长度为奇数的情况统一起来。
思路与算法
在中心扩展算法的过程中,我们能够得出每个位置的臂长。那么当我们要得出以下一个位置 i 的臂长时,能不能利用之前得到的信息呢?
答案是肯定的。具体来说,如果位置 j 的臂长为 length,并且有 j + length > i,
当在位置 i 开始进行中心拓展时,我们可以先找到 i 关于 j 的对称点 2 * j - i。那么如果点 2 * j - i 的臂长等于 n,我们就可以知道,点 i 的臂长至少为 min(j + length - i, n)。那么我们就可以直接跳过 i 到 i + min(j + length - i, n) 这部分,从 i + min(j + length - i, n) + 1 开始拓展。
我们只需要在中心扩展法的过程中记录右臂在最右边的回文字符串,将其中心作为 j,在计算过程中就能最大限度地避免重复计算。
那么现在还有一个问题:如何处理长度为偶数的回文字符串呢?
我们可以通过一个特别的操作将奇偶数的情况统一起来:我们向字符串的头尾以及每两个字符中间添加一个特殊字符 #,比如字符串 aaba 处理后会变成 #a#a#b#a#。那么原先长度为偶数的回文字符串 aa 会变成长度为奇数的回文字符串 #a#a#,而长度为奇数的回文字符串 aba 会变成长度仍然为奇数的回文字符串 #a#b#a#,我们就不需要再考虑长度为偶数的回文字符串了。
注意这里的特殊字符不需要是没有出现过的字母,我们可以使用任何一个字符来作为这个特殊字符。这是因为,当我们只考虑长度为奇数的回文字符串时,每次我们比较的两个字符奇偶性一定是相同的,所以原来字符串中的字符不会与插入的特殊字符互相比较,不会因此产生问题。
1 | class Solution: |
复杂度分析
- 时间复杂度:
,其中 是字符串的长度。由于对于每个位置,扩展要么从当前的最右侧臂长 right 开始,要么只会进行一步,而 right 最多向前走 步,因此算法的复杂度为 。 - 空间复杂度:
,我们需要 的空间记录每个位置的臂长。