问题描述
给定一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
约束条件:
1 <= s.length <= 1000s仅由数字和英文字母组成
错误的解法与分析
一个常见的解决此问题的思路是使用动态规划。我们定义 dp[i][j] 表示字符串 s 中从索引 i 到 j 的子串是否是回文串。
状态转移方程可以这样定义:
- 如果
s[i] == s[j]:- 如果子串长度小于等于 2(即
j - i < 2),那么dp[i][j]为true。 - 如果子串长度大于 2,那么
dp[i][j]取决于dp[i+1][j-1]是否为回文串。
- 如果子串长度小于等于 2(即
- 如果
s[i] != s[j],那么dp[i][j]为false。
以下是一个尝试实现该思路的 Go 代码,但它存在一个关键的错误:
1 | func longestPalindrome(s string) string { |
错误原因分析
主要的错误在于 DP 表的填充顺序。
在上述代码中,外层循环 i 从 0 遍历到 n-1,内层循环 j 从 i+1 遍历到 n-1。当我们计算 dp[i][j] 时,它依赖于 dp[i+1][j-1] 的值。
让我们考虑一个例子,比如计算 dp[0][3]。根据状态转移方程,它依赖于 dp[1][2]。
在当前的遍历顺序下:
i = 0:j = 1: 计算dp[0][1]j = 2: 计算dp[0][2](依赖dp[1][1])j = 3: 计算dp[0][3](依赖dp[1][2])
当 i=0, j=3 时,我们需要 dp[1][2] 的值。但是,dp[1][2] 会在 i=1, j=2 时才被计算。这意味着在计算 dp[0][3] 时,dp[1][2] 还没有被正确计算出来(其值仍为默认的 false),从而导致 dp[0][3] 的计算错误。
此外,end 变量的初始化为 1,当字符串长度为 1 时,如果 maxLen 没有更新(例如字符串本身就是单个字符,已经是回文),s[start : end+1] 会变成 s[0:2],这会导致数组越界。对于长度为 1 的字符串,start应为0,end应为0。
正确的解法与思路
为了解决填表顺序的问题,我们需要确保在计算 dp[i][j] 时,其依赖的 dp[i+1][j-1] 已经被计算过。这可以通过改变循环的遍历顺序来实现。
正确的思路是:
- 外层循环
i从n-1向下遍历到0。 - 内层循环
j从i+1向上遍历到n-1。
这样,当我们计算 dp[i][j] 时:
i的值比i+1小。j的值比j-1大。
由于i是从大到小遍历的,当计算dp[i][...]时,所有dp[i+1][...]的行都已经被计算完毕。因此,dp[i+1][j-1]的值是已知的。
同时,对于 end 的初始化,当 maxLen 为 1 时,start 和 end 都应该是 0(或者说,子串就是 s[0:1])。
正确的 Go 代码实现
1 | func longestPalindrome(s string) string { |
在这个修正后的版本中:
i从n-1递减到0。j从i+1递增到n-1。- 简化了
dp[i][j]的判断逻辑:如果s[i] == s[j],那么当子串长度为 2 (j-i == 1,等价于j-i < 2) 或者内部子串dp[i+1][j-1]为回文时,dp[i][j]才为true。 end的更新被移除了,最后直接通过start和maxLen来截取子串,这样更简洁且不易出错。对于maxLen初始化为1,start初始化为0,如果循环没有找到更长的回文串,会返回s[0:1],这是正确的。- 处理了
n < 2的边界情况。
学习总结
动态规划问题的核心之一在于确定正确的状态转移顺序。当一个状态 dp[i][j] 依赖于其他状态时(如本例中的 dp[i+1][j-1]),必须确保在计算当前状态之前,所有依赖的状态都已经被正确计算。
对于二维 DP 表,常见的遍历顺序有:
- 从上到下,从左到右。
- 从下到上,从左到右。
- 从上到下,从右到左。
- 从下到上,从右到左。
选择哪种顺序取决于状态之间的依赖关系。在本题中,由于 dp[i][j] 依赖于 dp[i+1][j-1](即左下方的单元格),因此从下往上(i 递减)、从左往右(j 递增,且 j > i)的遍历顺序是合适的。
另外,初始化边界条件和最终结果的提取方式也需要仔细考虑,以避免数组越界或逻辑错误。