docs: improve solution explanations for 最长回文子串 and 括号生成

- 添加思路推导部分,从暴力解法分析优化过程
- 增加详细的算法流程和Q&A形式的解释
- 添加执行过程演示和常见错误分析
- 完善边界条件和复杂度分析
- 保持原有的代码实现和进阶问题
This commit is contained in:
2026-03-08 21:32:02 +08:00
parent f0833d63cf
commit a5736a4db7
2 changed files with 939 additions and 140 deletions

View File

@@ -24,39 +24,335 @@
- `1 <= s.length <= 1000`
- `s` 仅由数字和英文字母组成
## 思路推导
### 暴力解法分析
**第一步:最直观的思路 - 枚举所有子串**
```python
def longestPalindrome_brute(s):
n = len(s)
max_len = 1
start = 0
# 枚举所有可能的子串
for i in range(n):
for j in range(i+1, n):
# 检查 s[i:j+1] 是否是回文
if isPalindrome(s, i, j) and (j - i + 1) > max_len:
start = i
max_len = j - i + 1
return s[start:start+max_len]
def isPalindrome(s, left, right):
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
```
**时间复杂度分析:**
- 枚举所有子串O(n²)(有 n(n+1)/2 个子串)
- 判断每个子串是否回文O(n)
- **总时间复杂度O(n³)**
**问题:**
- 当 n = 1000 时n³ = 10⁹ 次操作,会超时
- 存在大量重复判断,效率极低
### 优化思考 - 如何降低复杂度?
**核心观察:**
1. **暴力法的瓶颈**:每次判断回文都要重新遍历子串
2. **优化方向**:能否利用已计算的信息,避免重复判断?
**思路一:动态规划(空间换时间)**
关键问题:小问题的解能否帮助解决大问题?
```
如果 s[i:j] 是回文,那么:
- s[i+1:j-1] 也必须是回文
- s[i] == s[j]
```
这就是**最优子结构**
**思路二:中心扩展法(逆向思维)**
与其判断每个子串是否回文,不如:
1. 枚举每个可能的"中心"
2. 从中心向两边扩展
3. 扩展到不能扩展为止
为什么这样可行?
- 回文串是关于中心对称的
- 回文串的中心可能是:
- 一个字符(奇数长度):如 "aba",中心是 'b'
- 两个字符之间(偶数长度):如 "abba",中心在两个 'b' 之间
**思路三Manacher 算法(极致优化)**
中心扩展法的问题:同一个位置可能被访问多次
Manacher 的核心思想:
- 利用回文串的对称性
- 如果已经知道一个长回文串,可以快速计算出它内部对称位置的回文半径
- **时间复杂度降到 O(n)**
### 为什么这样思考?
**1. 降维思想**
```
暴力法:枚举起点 + 枚举终点 + 判断回文
优化:枚举中心 + 向两边扩展
从二维枚举(起点,终点) 降到 一维枚举(中心)
```
**2. 对称性利用**
```
回文串的定义:正读和反读相同
这意味着:
- 如果 s[left:right+1] 是回文
- 那么 s[left] == s[right]
- 且 s[left+1:right] 也是回文
```
**3. 中心扩展的优势**
```
暴力法:每个子串都要单独判断
中心扩展:一次扩展可以同时判断多个子串
例如 "babad"
以 'b'(index=0) 为中心扩展:
- "b" ✓
- "aba" ✓
- "bab" 不可能(越界)
```
## 解题思路
### 方法一:动态规划(推荐)
### 方法一:中心扩展法(推荐)
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
### 详细算法流程
**步骤1遍历所有可能的中心点**
```python
for i in range(len(s)):
# 奇数长度:以 s[i] 为中心
len1 = expand(s, i, i)
# 偶数长度:以 s[i] 和 s[i+1] 之间为中心
len2 = expand(s, i, i+1)
# 取较长的
max_len = max(max_len, len1, len2)
```
**Q: 为什么要考虑奇数和偶数两种情况?**
A: 因为回文串长度的奇偶性不同,中心位置也不同:
- 奇数长度 "aba":中心是单个字符 'b'
- 偶数长度 "abba":中心在两个字符之间
**步骤2从中心向两边扩展**
```python
def expand(s, left, right):
# 当不越界且字符相等时,继续扩展
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
# 返回回文串长度
# 注意循环结束时left 和 right 已经不满足条件
# 所以回文串是 s[left+1:right],长度为 right - left - 1
return right - left - 1
```
**Q: 为什么返回 right - left - 1**
A: 因为循环结束时:
- left 和 right 指向的位置要么越界,要么字符不等
- 实际的回文串是 s[left+1:right]
- 长度 = right - (left+1) = right - left - 1
举例:
```
s = "babad", 中心 i=1('a')
初始: left=1, right=1
第1次: left=0('b'), right=2('b'), 相等 ✓
第2次: left=-1(越界), right=3('a')
终止
返回: right - left - 1 = 3 - (-1) - 1 = 3
回文串: s[0:3] = "bab" ✓
```
**步骤3更新最大长度和起始位置**
```python
if current_len > max_len:
max_len = current_len
# 计算起始位置
start = i - (current_len - 1) // 2
```
**Q: 起始位置为什么是 i - (current_len - 1) // 2**
A: 需要根据奇偶性分别计算:
- 奇数长度:中心在字符上,起始 = i - len//2
- 偶数长度:中心在两个字符间,起始 = i - len//2 + 1
统一公式:`start = i - (len-1)//2`
举例:
```
奇数i=1, len=3 ("bab"的中心)
start = 1 - (3-1)//2 = 1 - 1 = 0 ✓
偶数i=1, len=2 ("bb"的中心在两个字符间)
start = 1 - (2-1)//2 = 1 - 0 = 1 ✓
```
### 关键细节说明
**细节1为什么需要两次扩展奇数和偶数**
```python
# 错误做法:只考虑奇数长度
for i in range(len(s)):
max_len = max(max_len, expand(s, i, i))
# 对于 "abba",会漏掉 "bb" 和 "abba"
```
```python
# 正确做法:奇偶都要考虑
for i in range(len(s)):
len1 = expand(s, i, i) # 奇数长度
len2 = expand(s, i, i+1) # 偶数长度
max_len = max(max_len, len1, len2)
```
**细节2为什么边界条件是 left >= 0 而不是 left > 0**
```python
# 错误写法
while left > 0 and right < len(s)-1 and s[left] == s[right]:
left -= 1
right += 1
# 问题:会漏掉边界上的回文串
# 例如 "aba" 的中心在 index=1
# 如果 left > 0就无法扩展到 index=0
```
```python
# 正确写法
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
```
**细节3如何处理单个字符的情况**
```python
def longestPalindrome(s):
if len(s) < 2:
return s # 直接返回,长度为 0 或 1 都是回文
# ... 后续逻辑
```
### 边界条件分析
**边界1空字符串或单字符**
```
输入: s = ""
输出: ""
原因: 空字符串也是回文串
输入: s = "a"
输出: "a"
原因: 单字符本身就是回文串
```
**边界2全部相同字符**
```
输入: s = "aaaa"
输出: "aaaa"
处理: 每个位置扩展都能得到长度 4
```
**边界3无回文串除单字符外**
```
输入: s = "abc"
输出: "a" 或 "b" 或 "c"
原因: 任意多字符子串都不是回文,返回任意单字符即可
```
**边界4多个最长回文串**
```
输入: s = "babad"
输出: "bab" 或 "aba"
原因: 题目允许返回任意一个最长回文串
```
### 复杂度分析(详细版)
**时间复杂度:**
```
- 外层循环:遍历 n 个字符 - O(n)
- 内层扩展:每个中心最多扩展 O(n) 次
- 总计O(n) × O(n) = O(n²)
为什么不是 O(n³)
- 暴力法:枚举起点 O(n) × 枚举终点 O(n) × 判断回文 O(n) = O(n³)
- 中心扩展:枚举中心 O(n) × 扩展 O(n) = O(n²)
- 关键:扩展是"一次判断,逐层扩展",而不是对每个子串重新判断
```
**空间复杂度:**
```
- 只使用了常数级别的变量O(1)
- 不需要额外的数组或哈希表
- 递归调用栈O(1)(虽然看起来像递归,但深度最多 O(n)
```
### 方法二:动态规划
**核心思想:**使用二维数组 `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]` 为真
```
dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1])
**算法步骤:**
1. 初始化 `dp` 数组,所有单个字符都是回文串
2. 按长度递增的顺序遍历所有子串
3. 更新最长回文子串的起始位置和长度
解释:
1. s[i] == s[j]:首尾字符必须相等
2. j - i < 2子串长度小于 3如 "a", "aa")必然是回文
3. dp[i+1][j-1]:去掉首尾后的子串也必须是回文
```
### 方法二:中心扩展法(最优)
**Q: 为什么按长度递增的顺序遍历?**
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
A: 因为计算 `dp[i][j]` 需要 `dp[i+1][j-1]`,而 `dp[i+1][j-1]` 对应更短的子串。
**算法步骤:**
1. 遍历每个字符作为中心点
2. 从中心点向两边扩展,直到不再是回文串
3. 记录最长的回文串
4. 需要考虑奇数长度和偶数长度两种情况
### 方法三Manacher 算法(最优)
**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。
**算法步骤:**
1. 在字符串中插入特殊字符(如 `#`),统一处理奇偶长度
2. 使用数组 `P` 记录以每个字符为中心的最长回文半径
3. 利用对称性快速计算回文半径
```python
# 按长度从 2 到 n 遍历
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# 计算 dp[i][j]
```
## 代码实现
@@ -134,6 +430,8 @@ func main() {
}
```
### Go 实现(动态规划)
```go
func longestPalindromeDP(s string) string {
if len(s) < 2 {
@@ -174,89 +472,150 @@ func longestPalindromeDP(s string) string {
}
```
## 执行过程演示
`s = "babad"` 为例:
```
初始状态: start=0, maxLen=1
i=0, 字符='b':
奇数扩展: expand(s, 0, 0)
left=0, right=0: 'b'=='b' ✓
left=-1, right=1: 越界,停止
返回: 1 - (-1) - 1 = 1
偶数扩展: expand(s, 0, 1)
left=0, right=1: 'b'!='a' ✗
返回: 1 - 0 - 1 = 0
maxLen = max(1, 1, 0) = 1
i=1, 字符='a':
奇数扩展: expand(s, 1, 1)
left=1, right=1: 'a'=='a' ✓
left=0, right=2: 'b'=='b' ✓
left=-1, right=3: 越界,停止
返回: 3 - (-1) - 1 = 3
偶数扩展: expand(s, 1, 2)
left=1, right=2: 'a'!='b' ✗
返回: 2 - 1 - 1 = 0
更新: maxLen=3, start=1-(3-1)/2=0
当前最长: s[0:3]="bab"
i=2, 字符='b':
奇数扩展: expand(s, 2, 2)
left=2, right=2: 'b'=='b' ✓
left=1, right=3: 'a'=='a' ✓
left=0, right=4: 'b'!='d' ✗
返回: 4 - 0 - 1 = 3
偶数扩展: expand(s, 2, 3)
left=2, right=3: 'b'!='a' ✗
返回: 3 - 2 - 1 = 0
maxLen = max(3, 3, 0) = 3 (保持不变)
i=3, 字符='a':
奇数扩展: expand(s, 3, 3)
left=3, right=3: 'a'=='a' ✓
left=2, right=4: 'b'!='d' ✗
返回: 4 - 2 - 1 = 1
偶数扩展: expand(s, 3, 4)
left=3, right=4: 'a'!='d' ✗
返回: 4 - 3 - 1 = 0
maxLen = max(3, 1, 0) = 3 (保持不变)
i=4, 字符='d':
奇数扩展: expand(s, 4, 4)
left=4, right=4: 'd'=='d' ✓
left=3, right=5: 越界,停止
返回: 5 - 3 - 1 = 1
偶数扩展: expand(s, 4, 5)
right=5 越界,直接返回 0
maxLen = max(3, 1, 0) = 3 (保持不变)
最终结果: s[0:3] = "bab"
```
## 常见错误
### 错误1只考虑奇数长度回文
**错误写法:**
```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
func longestPalindromeWrong(s string) string {
for i := 0; i < len(s); i++ {
len := expand(s, i, i) // 只考虑奇数
// ...
}
}
```
## 复杂度分析
**正确写法:**
```go
func longestPalindrome(s string) string {
for i := 0; i < len(s); i++ {
len1 := expand(s, i, i) // 奇数
len2 := expand(s, i, i+1) // 偶数
maxLen = max(maxLen, len1, len2)
}
}
```
### 中心扩展法
**原因:**会漏掉偶数长度的回文串,如 "abba"。
- **时间复杂度:** O(n²)
- 外层循环遍历 n 个字符
- 内层扩展最多 O(n) 次
- 总时间复杂度O(n²)
### 错误2边界条件错误
- **空间复杂度:** O(1)
- 只使用了常数级别的额外空间
**错误写法:**
```go
func expand(s string, left, right int) int {
for left > 0 && right < len(s)-1 && s[left] == s[right] {
left--
right++
}
return right - left - 1
}
```
### 动态规划
**正确写法:**
```go
func expand(s string, left, right int) int {
for left >= 0 && right < len(s) && s[left] == s[right] {
left--
right++
}
return right - left - 1
}
```
- **时间复杂度:** O(n²)
- 需要填充 n×n 的 dp 数组
- 但由于剪枝,实际复杂度约为 O(n²/2)
**原因:**边界字符也是回文串的一部分,需要判断。
- **空间复杂度:** O(n²)
- 需要存储 n×n 的 dp 数组
### 错误3起始位置计算错误
### Manacher 算法
**错误写法:**
```go
if currentLen > maxLen {
maxLen = currentLen
start = i - currentLen/2 // 错误!
}
```
- **时间复杂度:** O(n)
- 只需遍历字符串一次
- 利用对称性避免重复计算
**正确写法:**
```go
if currentLen > maxLen {
maxLen = currentLen
start = i - (currentLen-1)/2 // 正确
}
```
- **空间复杂度:** O(n)
- 需要存储处理后的字符串和半径数组
**原因:**需要考虑奇偶性,统一公式是 `i - (len-1)/2`
## 进阶问题