# 最长回文子串 (Longest Palindromic Substring) ## 题目描述 给你一个字符串 `s`,找到 `s` 中最长的回文子串。 ### 示例 **示例 1:** ``` 输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。 ``` **示例 2:** ``` 输入:s = "cbbd" 输出:"bb" ``` ### 约束条件 - `1 <= s.length <= 1000` - `s` 仅由数字和英文字母组成 ## 解题思路 ### 方法一:动态规划(推荐) **核心思想:**使用二维数组 `dp[i][j]` 表示 `s[i:j+1]` 是否为回文串。 **状态转移方程:** - `dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1])` - 如果 `s[i] == s[j]` 且 `dp[i+1][j-1]` 为真(或子串长度小于3),则 `dp[i][j]` 为真 **算法步骤:** 1. 初始化 `dp` 数组,所有单个字符都是回文串 2. 按长度递增的顺序遍历所有子串 3. 更新最长回文子串的起始位置和长度 ### 方法二:中心扩展法(最优) **核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。 **算法步骤:** 1. 遍历每个字符作为中心点 2. 从中心点向两边扩展,直到不再是回文串 3. 记录最长的回文串 4. 需要考虑奇数长度和偶数长度两种情况 ### 方法三:Manacher 算法(最优) **核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。 **算法步骤:** 1. 在字符串中插入特殊字符(如 `#`),统一处理奇偶长度 2. 使用数组 `P` 记录以每个字符为中心的最长回文半径 3. 利用对称性快速计算回文半径 ## 代码实现 ### Go 实现(中心扩展法) ```go package main import "fmt" func longestPalindrome(s string) string { if len(s) < 2 { return s } start, maxLen := 0, 1 for i := 0; i < len(s); i++ { // 奇数长度:以当前字符为中心 len1 := expandAroundCenter(s, i, i) // 偶数长度:以当前字符和下一个字符之间为中心 len2 := expandAroundCenter(s, i, i+1) currentLen := max(len1, len2) if currentLen > maxLen { maxLen = currentLen start = i - (currentLen-1)/2 } } return s[start : start+maxLen] } func expandAroundCenter(s string, left, right int) int { for left >= 0 && right < len(s) && s[left] == s[right] { left-- right++ } return right - left - 1 } func max(a, b int) int { if a > b { return a } return b } // 测试用例 func main() { // 测试用例1 s1 := "babad" fmt.Printf("输入: %s\n", s1) fmt.Printf("输出: %s\n", longestPalindrome(s1)) // 测试用例2 s2 := "cbbd" fmt.Printf("\n输入: %s\n", s2) fmt.Printf("输出: %s\n", longestPalindrome(s2)) // 测试用例3: 单个字符 s3 := "a" fmt.Printf("\n输入: %s\n", s3) fmt.Printf("输出: %s\n", longestPalindrome(s3)) // 测试用例4: 全部相同 s4 := "aaaa" fmt.Printf("\n输入: %s\n", s4) fmt.Printf("输出: %s\n", longestPalindrome(s4)) // 测试用例5: 无回文 s5 := "abc" fmt.Printf("\n输入: %s\n", s5) fmt.Printf("输出: %s\n", longestPalindrome(s5)) } ``` ### Java 实现(中心扩展法) ### Go 实现(动态规划) ```go func longestPalindromeDP(s string) string { if len(s) < 2 { return s } n := len(s) dp := make([][]bool, n) for i := range dp { dp[i] = make([]bool, n) } start, maxLen := 0, 1 // 初始化:所有单个字符都是回文串 for i := 0; i < n; i++ { dp[i][i] = true } // 按长度递增的顺序遍历 for length := 2; length <= n; length++ { for i := 0; i <= n-length; i++ { j := i + length - 1 if s[i] == s[j] { if length == 2 || dp[i+1][j-1] { dp[i][j] = true if length > maxLen { maxLen = length start = i } } } } } return s[start : start+maxLen] } ``` ### Java 实现(动态规划) ### Go 实现(Manacher 算法) ```go func longestPalindromeManacher(s string) string { if len(s) < 2 { return s } // 预处理:插入特殊字符 t := "#" for i := 0; i < len(s); i++ { t += string(s[i]) + "#" } n := len(t) p := make([]int, n) center, right := 0, 0 maxCenter, maxLen := 0, 0 for i := 0; i < n; i++ { if i < right { mirror := 2*center - i p[i] = min(right-i, p[mirror]) } // 尝试扩展 for i+p[i]+1 < n && i-p[i]-1 >= 0 && t[i+p[i]+1] == t[i-p[i]-1] { p[i]++ } // 更新中心和右边界 if i+p[i] > right { center = i right = i + p[i] } // 更新最大回文串 if p[i] > maxLen { maxLen = p[i] maxCenter = i } } // 计算原字符串中的起始位置 start := (maxCenter - maxLen) / 2 return s[start : start+maxLen] } func min(a, b int) int { if a < b { return a } return b } ``` ## 复杂度分析 ### 中心扩展法 - **时间复杂度:** O(n²) - 外层循环遍历 n 个字符 - 内层扩展最多 O(n) 次 - 总时间复杂度:O(n²) - **空间复杂度:** O(1) - 只使用了常数级别的额外空间 ### 动态规划 - **时间复杂度:** O(n²) - 需要填充 n×n 的 dp 数组 - 但由于剪枝,实际复杂度约为 O(n²/2) - **空间复杂度:** O(n²) - 需要存储 n×n 的 dp 数组 ### Manacher 算法 - **时间复杂度:** O(n) - 只需遍历字符串一次 - 利用对称性避免重复计算 - **空间复杂度:** O(n) - 需要存储处理后的字符串和半径数组 ## 进阶问题 ### Q1: 如何找到所有回文子串? **A:** 修改中心扩展法,找到每个回文子串都记录下来。 ```go func findAllPalindromes(s string) []string { result := []string{} for i := 0; i < len(s); i++ { // 奇数长度 l, r := i, i for l >= 0 && r < len(s) && s[l] == s[r] { result = append(result, s[l:r+1]) l-- r++ } // 偶数长度 l, r = i, i+1 for l >= 0 && r < len(s) && s[l] == s[r] { result = append(result, s[l:r+1]) l-- r++ } } return result } ``` ### Q2: 如何判断一个字符串是否可以通过重新排列成为回文串? **A:** 统计每个字符出现的次数,最多只能有一个字符出现奇数次。 ```go func canPermutePalindrome(s string) bool { count := make(map[rune]int) for _, c := range s { count[c]++ } oddCount := 0 for _, c := range count { if c%2 == 1 { oddCount++ } } return oddCount <= 1 } ``` ### Q3: 最长回文子序列(非连续)如何求解? **A:** 使用动态规划,`dp[i][j]` 表示 `s[i:j+1]` 的最长回文子序列长度。 ```go func longestPalindromeSubseq(s string) int { n := len(s) dp := make([][]int, n) for i := range dp { dp[i] = make([]int, n) dp[i][i] = 1 } for length := 2; length <= n; length++ { for i := 0; i <= n-length; i++ { j := i + length - 1 if s[i] == s[j] { dp[i][j] = dp[i+1][j-1] + 2 } else { dp[i][j] = max(dp[i+1][j], dp[i][j-1]) } } } return dp[0][n-1] } ``` ## P7 加分项 ### 1. 深度理解:为什么中心扩展法最优? **对比分析:** - **暴力法:** O(n³) - 枚举所有子串 O(n²),判断是否回文 O(n) - **动态规划:** O(n²) 时间,O(n²) 空间 - **中心扩展法:** O(n²) 时间,O(1) 空间 - **Manacher 算法:** O(n) 时间,O(n) 空间 **选择建议:** - **面试:** 中心扩展法 - 代码简洁,易于实现 - **实际应用:** Manacher 算法 - 性能最优 - **学习:** 都要掌握,理解不同思路 ### 2. 实战扩展:回文串相关算法 #### 短回文串构造 **LeetCode 214:** 给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。 ```go func shortestPalindrome(s string) string { if len(s) < 2 { return s } // 找到最长的回文前缀 n := len(s) rev := reverse(s) combined := s + "#" + rev // KMP 算法计算最长公共前后缀 pi := make([]int, len(combined)) for i := 1; i < len(combined); i++ { j := pi[i-1] for j > 0 && combined[i] != combined[j] { j = pi[j-1] } if combined[i] == combined[j] { j++ } pi[i] = j } // 添加剩余字符的逆序 add := rev[:n-pi[len(combined)-1]] return add + s } func reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes) } ``` ### 3. 变形题目 #### 变形1:回文子串个数 **LeetCode 647:** 给定一个字符串,计算这个字符串中有多少个回文子串。 ```go func countSubstrings(s string) int { count := 0 for i := 0; i < len(s); i++ { // 奇数长度 count += expandAroundCenterCount(s, i, i) // 偶数长度 count += expandAroundCenterCount(s, i, i+1) } return count } func expandAroundCenterCount(s string, left, right int) int { count := 0 for left >= 0 && right < len(s) && s[left] == s[right] { count++ left-- right++ } return count } ``` #### 变形2:分割回文串 **LeetCode 131:** 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。 ```go func partition(s string) [][]string { result := [][]string{} current := []string{} var backtrack func(start int) backtrack = func(start int) { if start == len(s) { temp := make([]string, len(current)) copy(temp, current) result = append(result, temp) return } for end := start + 1; end <= len(s); end++ { if isPalindrome(s[start:end]) { current = append(current, s[start:end]) backtrack(end) current = current[:len(current)-1] } } } backtrack(0) return result } func isPalindrome(s string) bool { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { if s[i] != s[j] { return false } } return true } ``` ### 4. 优化技巧 #### 优化1:提前终止 如果找到的回文串已经接近最大可能长度,可以提前终止。 ```go func longestPalindromeOptimized(s string) string { if len(s) < 2 { return s } start, maxLen := 0, 1 for i := 0; i < len(s); i++ { // 提前终止:不可能找到更长的回文串了 if maxLen >= 2*(len(s)-i) { break } len1 := expandAroundCenter(s, i, i) len2 := expandAroundCenter(s, i, i+1) currentLen := max(len1, len2) if currentLen > maxLen { maxLen = currentLen start = i - (currentLen-1)/2 } } return s[start : start+maxLen] } ``` ### 5. 实际应用场景 - **DNA 序列分析:** 寻找重复序列 - **文本编辑:** 检查拼写和语法 - **数据压缩:** 利用重复模式 - **模式匹配:** 查找对称模式 ### 6. 面试技巧 **面试官可能会问:** 1. "为什么中心扩展法比动态规划更好?" 2. "Manacher 算法的核心思想是什么?" 3. "如何处理 unicode 字符?" **回答要点:** 1. 中心扩展法空间复杂度 O(1),动态规划需要 O(n²) 2. Manacher 算法利用回文串的对称性,避免重复计算 3. 使用 rune 类型处理 unicode 字符 ### 7. 相关题目推荐 - LeetCode 5: 最长回文子串(本题) - LeetCode 125: 验证回文串 - LeetCode 131: 分割回文串 - LeetCode 214: 最短回文串 - LeetCode 516: 最长回文子序列 - LeetCode 647: 回文子串个数