【力扣】最长回文字符串
给你一个字符串 s,找到 s 中最长的回文子串。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
「暴力算法」是基础,「动态规划」必须掌握,「中心扩散」方法要会写;
方法一:暴力匹配 (Brute Force)
根据回文子串的定义,枚举所有长度大于等于 22 的子串,依次判断它们是否是回文;
在具体实现时,可以只针对大于“当前得到的最长回文子串长度”的子串进行“回文验证”;
在记录最长回文子串的时候,可以只记录“当前子串的起始位置”和“子串长度”,不必做截取。这一步我们放在后面的方法中实现。
1 class Solution: 2 # 暴力匹配(超时) 3 def longestPalindrome(self, s: str) -> str: 4 # 特判 5 size = len(s) 6 if size < 2: 7 return s 8 9 max_len = 1 10 res = s[0] 11 12 # 枚举所有长度大于等于 2 的子串 13 for i in range(size - 1): 14 for j in range(i + 1, size): 15 if j - i + 1 > max_len and self.__valid(s, i, j): 16 max_len = j - i + 1 17 res = s[i:j + 1] 18 return res 19 20 def __valid(self, s, left, right): 21 # 验证子串 s[left, right] 是否为回文串 22 while left < right: 23 if s[left] != s[right]: 24 return False 25 left += 1 26 right -= 1 27 return True 28 29 # 超时测试用例 30 31 32 作者:liweiwei1419 33 链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/ 34 来源:力扣(LeetCode)
方法二:动态规划
下面是「动态规划』问题的思考路径,供大家参考。
1、思考状态(重点)
状态的定义,先尝试「题目问什么,就把什么设置为状态」;
然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。
2、思考状态转移方程(核心、难点)
状态转移方程是非常重要的,是动态规划的核心,也是难点;
常见的推导技巧是:分类讨论。即:对状态空间进行分类;
归纳「状态转移方程」是一个很灵活的事情,通常是具体问题具体分析;
除了掌握经典的动态规划问题以外,还需要多做题;
如果是针对面试,请自行把握难度。掌握常见问题的动态规划解法,理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;
「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。
3、思考初始化
初始化是非常重要的,一步错,步步错。初始化状态一定要设置对,才可能得到正确的结果。
角度 1:直接从状态的语义出发;
角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;
角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。
4、思考输出
有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。
5、思考优化空间(也可以叫做表格复用)
「优化空间」会使得代码难于理解,且是的「状态」丢失原来的语义,初学的时候可以不一步到位。先把代码写正确是更重要;
「优化空间」在有一种情况下是很有必要的,那就是状态空间非常庞大的时候(处理海量数据),此时空间不够用,就必须「优化空间」;
非常经典的「优化空间」的典型问题是「0-1 背包」问题和「完全背包」问题。
(下面是这道问题「动态规划」方法的分析)
这道题比较烦人的是判断回文子串。因此需要一种能够快速判断原字符串的所有子串是否是回文子串的方法,于是想到了「动态规划」。
「动态规划」的一个关键的步骤是想清楚「状态如何转移」。事实上,「回文」天然具有「状态转移」性质。
一个回文去掉两头以后,剩下的部分依然是回文(这里暂不讨论边界情况);
依然从回文串的定义展开讨论:
如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;
如果一个字符串的头尾两个字符相等,才有必要继续判断下去。
如果里面的子串是回文,整体就是回文串;
如果里面的子串不是回文串,整体就不是回文串。
即:在头尾字符相等的情况下,里面子串的回文性质据定了整个子串的回文性质,这就是状态转移。因此可以把「状态」定义为原字符串的一个子串是否为回文子串。
第 1 步:定义状态
dp[i][j] 表示子串 s[i..j] 是否为回文子串,这里子串 s[i..j] 定义为左闭右闭区间,可以取到 s[i] 和 s[j]。
第 2 步:思考状态转移方程
在这一步分类讨论(根据头尾字符是否相等),根据上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
说明:
「动态规划」事实上是在填一张二维表格,由于构成子串,因此 i 和 j 的关系是 i <= j ,因此,只需要填这张表格对角线以上的部分。
看到 dp[i + 1][j - 1] 就得考虑边界情况。
边界条件是:表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3。
这个结论很显然:j - i < 3 等价于 j - i + 1 < 4,即当子串 s[i..j] 的长度等于 2 或者等于 3 的时候,其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。
如果子串 s[i + 1..j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,显然是回文;
如果子串 s[i + 1..j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
因此,在 s[i] == s[j] 成立和 j - i < 3 的前提下,直接可以下结论,dp[i][j] = true,否则才执行状态转移。
第 3 步:考虑初始化
初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 true,即 dp[i][i] = true 。
事实上,初始化的部分都可以省去。因为只有一个字符的时候一定是回文,dp[i][i] 根本不会被其它状态值所参考。
第 4 步:考虑输出
只要一得到 dp[i][j] = true,就记录子串的长度和起始位置,没有必要截取,这是因为截取字符串也要消耗性能,记录此时的回文子串的「起始位置」和「回文长度」即可。
第 5 步:考虑优化空间
因为在填表的过程中,只参考了左下方的数值。事实上可以优化,但是增加了代码编写和理解的难度,丢失可读和可解释性。在这里不优化空间。
注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要。
大家能够可以自己动手,画一下表格,相信会对「动态规划」作为一种「表格法」有一个更好的理解。
作者:liweiwei1419 讲的比较详细可以复看
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
思路与算法
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 $\textrm{``ababa''}$,如果我们已经知道 $\textrm{``bab''} $是回文串,那么 $\textrm{``ababa''} $一定是回文串,这是因为它的首尾两个字母都是 $\textrm{``a''}$。
根据这样的思路,我们就可以用动态规划的方法解决本题。我们用 $P(i,j) $表示字符串 $s$ 的第 $i$到 $j$个字母组成的串(下文表示成$ s[i:j]$)是否为回文串:
$P(i,j) = \begin{cases} \text{true,} &\quad\text{如果子串~} S_i \dots S_j \text{~是回文串}\\ \text{false,} &\quad\text{其它情况} \end{cases}$
这里的「其它情况」包含两种可能性:
$s[i, j]$本身不是一个回文串;
$i > j$,此时$ s[i, j]$ 本身不合法。
那么我们就可以写出动态规划的状态转移方程:
$P(i, j) = P(i+1, j-1) \wedge (S_i == S_j)$
也就是说,只有 $s[i+1:j-1] $是回文串,并且 $s$ 的第 $i $和 $j$个字母相同时,$s[i:j]$才会是回文串。
上文的所有讨论是建立在子串长度大于 2 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 1 或 2。对于长度为 1的子串,它显然是个回文串;对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
$\begin{cases} P(i, i) = \text{true} \\ P(i, i+1) = ( S_i == S_{i+1} ) \end{cases}$
根据这个思路,我们就可以完成动态规划了,最终的答案即为所有 $P(i, j) = \text{true} $中 $j-i+1$(即子串长度)的最大值。注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
1 class Solution: 2 def longestPalindrome(self, s: str) -> str: 3 n = len(s) 4 dp = [[False] * n for _ in range(n)] 5 ans = "" 6 # 枚举子串的长度 l+1 7 for l in range(n): 8 # 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置 9 for i in range(n): 10 j = i + l 11 if j >= len(s): 12 break 13 if l == 0: 14 dp[i][j] = True 15 elif l == 1: 16 dp[i][j] = (s[i] == s[j]) 17 else: 18 dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j]) 19 if dp[i][j] and l + 1 > len(ans): 20 ans = s[i:j+1] 21 return ans 22 23 作者:LeetCode-Solution 24 链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/ 25 来源:力扣(LeetCode)
中心扩展算法
思路与算法
我们仔细观察一下方法一中的状态转移方程:
$\begin{cases} P(i, i) &=\quad \text{true} \\ P(i, i+1) &=\quad ( S_i == S_{i+1} ) \\ P(i, j) &=\quad P(i+1, j-1) \wedge (S_i == S_j) \end{cases}$
找出其中的状态转移链:
$P(i, j) \leftarrow P(i+1, j-1) \leftarrow P(i+2, j-2) \leftarrow \cdots \leftarrow \text{某一边界情况}$
可以发现,所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。
边界情况即为子串长度为 1 或 2 的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。如果两边的字母相同,我们就可以继续扩展,例如从$ P(i+1,j-1) $扩展到 $P(i,j)$;如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。
聪明的读者此时应该可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。方法二的本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。
1 class Solution: 2 def expandAroundCenter(self, s, left, right): 3 while left >= 0 and right < len(s) and s[left] == s[right]: 4 left -= 1 5 right += 1 6 return left + 1, right - 1 7 8 def longestPalindrome(self, s: str) -> str: 9 start, end = 0, 0 10 for i in range(len(s)): 11 left1, right1 = self.expandAroundCenter(s, i, i) 12 left2, right2 = self.expandAroundCenter(s, i, i + 1) 13 if right1 - left1 > end - start: 14 start, end = left1, right1 15 if right2 - left2 > end - start: 16 start, end = left2, right2 17 return s[start: end + 1] 18 19 作者:LeetCode-Solution 20 链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/ 21 来源:力扣(LeetCode)