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

@@ -22,54 +22,353 @@
- `1 <= n <= 8`
## 思路推导
### 暴力解法分析
**第一步:最直观的思路 - 生成所有可能的组合**
```python
def generateParenthesis_brute(n):
# 生成所有长度为 2n 的括号组合
all_combinations = []
generate_all("", 2*n, all_combinations)
# 过滤出有效的组合
valid_combinations = []
for combo in all_combinations:
if is_valid(combo):
valid_combinations.append(combo)
return valid_combinations
def generate_all(current, max_len, result):
if len(current) == max_len:
result.append(current)
return
generate_all(current + "(", max_len, result)
generate_all(current + ")", max_len, result)
def is_valid(s):
balance = 0
for char in s:
if char == "(":
balance += 1
else:
balance -= 1
if balance < 0:
return False
return balance == 0
```
**时间复杂度分析:**
- 生成所有组合:每个位置有 2 种选择,共 2^(2n) 个组合
- 判断每个组合是否有效O(n)
- **总时间复杂度O(n × 2^(2n))**
**问题:**
- 生成了大量无效组合
- 对于 n = 82^(16) = 65536 个组合,但有效的只有卡特兰数 C(8) = 1430 个
- 效率极低,大部分计算都浪费了
### 优化思考 - 如何避免生成无效组合?
**核心观察:**
1. **暴力法的瓶颈**:生成了大量无效组合,最后才过滤
2. **优化方向**:能否在生成过程中就避免无效组合?
**关键问题:什么是无效组合?**
无效组合的特征:
1. 右括号数量超过左括号数量:")()("
2. 最终左右括号数量不相等:"((()"
**思路:在生成过程中实时检查**
与其生成所有组合再过滤,不如:
1. 在添加每个括号时就检查是否合法
2. 如果不合法,直接跳过这个分支(剪枝)
3. 只生成有可能合法的组合
```python
def generateParenthesis_optimized(n):
result = []
def backtrack(current, open_count, close_count):
# 终止条件
if len(current) == 2 * n:
result.append(current)
return
# 关键剪枝条件
# 1. 左括号数量不能超过 n
if open_count < n:
backtrack(current + "(", open_count + 1, close_count)
# 2. 右括号数量不能超过左括号数量
if close_count < open_count:
backtrack(current + ")", open_count, close_count + 1)
backtrack("", 0, 0)
return result
```
### 为什么这样思考?
**1. 剪枝思想Pruning**
```
暴力法:生成所有 2^(2n) 个组合,然后过滤
优化:在生成过程中就剪掉不可能的分支
效果:只生成卡特兰数 C(n) 个有效组合
```
**2. 有效括号的本质**
```
有效括号的两个充要条件:
1. 任何时候,左括号数量 >= 右括号数量
2. 最终,左括号数量 = 右括号数量 = n
这意味着:
- 可以添加左括号的条件open < n
- 可以添加右括号的条件close < open
```
**3. 递归树视角**
```
n = 2 时的递归树:
"" (open=0, close=0)
├── "(" (open=1, close=0)
│ ├── "((" (open=2, close=0)
│ │ └── "(()" (open=2, close=1)
│ │ └── "(())" (open=2, close=2) ✓
│ └── "()" (open=1, close=1)
│ └── "()(" (open=2, close=1)
│ └── "()()" (open=2, close=2) ✓
└── ")" (open=0, close=0)
✗ 剪枝close 不能 > open
只生成了 2 个有效组合,而不是 2^4 = 16 个!
```
## 解题思路
### 方法一:回溯法(推荐)
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性
1. 左括号数量不能超过 n
2. 右括号数量不能超过左括号数量
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性
**算法步骤:**
1. 初始化结果数组 `result` 和当前字符串 `current`
2. 定义回溯函数 `backtrack(open, close)`
- `open`:已使用的左括号数量
- `close`:已使用的右括号数量
3. 终止条件:`len(current) == 2 * n`,将 `current` 加入 `result`
4. 选择条件:
- 如果 `open < n`,可以添加左括号
- 如果 `close < open`,可以添加右括号
5. 递归调用后撤销选择(回溯)
### 详细算法流程
**为什么这样做?**
- 通过限制 `close < open`,保证任何时候右括号数量不超过左括号数量
- 通过限制 `open < n`,保证左括号数量不超过 n
- 这样生成的所有组合都是有效的
**步骤1理解有效括号的生成规则**
### 方法二DFS 深度优先搜索
```python
# 规则1左括号数量不能超过 n
if open < n:
可以添加 '('
**核心思想:**与回溯法类似,但使用更纯粹的 DFS 思想。将问题看作在二叉树中搜索。
# 规则2右括号数量不能超过左括号数量
if close < open:
可以添加 ')'
```
**算法步骤:**
1. 构建一个递归树,每个节点代表一个状态
2. 从根节点开始,每次可以选择添加左括号或右括号
3. 剪枝:不符合条件的分支直接跳过
4. 到达叶子节点(长度为 2n记录结果
**Q: 为什么右括号数量不能超过左括号数量?**
### 方法三:动态规划
A: 因为在任何前缀中,如果右括号多于左括号,就不可能通过后续添加括号使其变成有效括号。
**核心思想:**利用卡特兰数Catalan Number的性质。n 对括号的有效组合数等于第 n 个卡特兰数。
举例:
```
")" → 不可能变有效,因为第一个字符就是右括号
"())(" → 中间的 "()" 后面是 ")",右括号已经多余了
```
**步骤2设计回溯函数**
```python
def backtrack(current, open, close):
# 终止条件:生成了足够的括号
if len(current) == 2 * n:
result.append(current)
return
# 选择1添加左括号如果可以
if open < n:
backtrack(current + "(", open + 1, close)
# 选择2添加右括号如果可以
if close < open:
backtrack(current + ")", open, close + 1)
```
**Q: 为什么不需要显式地撤销选择(回溯)?**
A: 因为我们使用的是字符串拼接 `current + "("`,这会创建新的字符串,不会修改原来的 `current`。每次递归调用都是独立的,不需要手动撤销。
如果要使用列表优化性能:
```python
def backtrack(current, open, close):
if len(current) == 2 * n:
result.append("".join(current))
return
if open < n:
current.append("(") # 做选择
backtrack(current, open + 1, close)
current.pop() # 撤销选择
if close < open:
current.append(")") # 做选择
backtrack(current, open, close + 1)
current.pop() # 撤销选择
```
**步骤3初始化并启动回溯**
```python
result = []
backtrack("", 0, 0)
return result
```
**Q: 为什么初始状态是 open=0, close=0**
A: 因为我们从空字符串开始,还没有添加任何括号。
### 关键细节说明
**细节1为什么终止条件是 `len(current) == 2 * n`**
```python
# 错误理解:应该是 len(current) == n
# 错误原因n 是括号的对数,每对有 2 个括号
# 正确理解:总长度是 2n
# 例如 n=3最终字符串长度是 6"((()))"
```
**细节2为什么两个 if 是独立的,而不是 if-else**
```python
# 错误写法if-else
if open < n:
backtrack(current + "(", open + 1, close)
else:
backtrack(current + ")", open, close + 1)
# 问题:这样会导致每步只能添加一种括号
# 正确写法:两个独立的 if
if open < n:
backtrack(current + "(", open + 1, close)
if close < open:
backtrack(current + ")", open, close + 1)
```
**为什么?**
- 因为在满足两个条件的情况下,我们可以选择添加左括号或右括号
- 这是两种不同的选择,需要分别探索
**细节3为什么判断条件是 `close < open` 而不是 `close <= open`**
```python
# 错误写法
if close <= open: # ❌
backtrack(current + ")", open, close + 1)
# 问题:当 close == open 时,不能添加右括号
# 例如current="()", open=1, close=1
# 如果再添加 ")",变成 "()()",这是错误的
# 正确写法
if close < open: # ✓
backtrack(current + ")", open, close + 1)
```
### 边界条件分析
**边界1n = 1**
```
输入n = 1
输出:["()"]
过程:
"" → "(" → "()"
终止len("()") = 2 = 2*1
```
**边界2n = 8最大值**
```
输入n = 8
输出1430 个组合(卡特兰数 C(8)
注意:虽然约束是 n <= 8但算法可以处理更大的 n
```
**边界3某个时刻右括号用完了**
```
状态current="(()(", open=3, close=1
分析:
- 不能添加 ")":因为 close(1) < open(3),可以添加
- 添加后current="(()()", open=3, close=2
- 继续添加 ")"current="(()())", open=3, close=3 ✓
```
### 复杂度分析(详细版)
**时间复杂度:**
```
- 理论最坏情况:每个节点有 2 个分支,深度为 2n → O(2^(2n))
- 实际情况:由于剪枝,只有卡特兰数 C(n) 个有效节点
- 卡特兰数公式C(n) = (2n)! / ((n+1)! × n!)
- 渐近复杂度C(n) ≈ 4^n / (n^(3/2) × √π)
- **时间复杂度O(4^n / √n)**
为什么是 4^n 而不是 2^(2n)
- 数学上 2^(2n) = 4^n
- 但由于剪枝,实际是 O(4^n / √n),比 O(4^n) 小得多
```
**空间复杂度:**
```
- 递归栈深度:最多 2n 层(每添加一个括号递归一次)- O(n)
- 存储结果O(C(n))(卡特兰数)- 通常不计入空间复杂度
- **空间复杂度O(n)**(不计结果存储)
```
### 方法二:动态规划
**核心思想:**利用卡特兰数的递推关系。
**递推公式:**
- `dp[n]` 表示 n 对括号的所有有效组合
- `dp[n] = "(" + dp[i] + ")" + dp[n-1-i]`,其中 `i` 从 0 到 n-1
```
dp[n] = "(" + dp[i] + ")" + dp[n-1-i]
**算法步骤:**
1. 初始化 `dp[0] = [""]`
2. 对于 `i` 从 1 到 n
- 对于 `j` 从 0 到 i-1
-`dp[j]` 的每个组合加上一对括号,再拼接 `dp[i-1-j]` 的每个组合
3. 返回 `dp[n]`
其中 i 从 0 到 n-1
解释
- dp[n] 表示 n 对括号的所有有效组合
- 每个组合可以看作:一对括号包裹着 i 对括号,后面跟着 n-1-i 对括号
```
**Q: 为什么这样递推?**
A: 任何有效的 n 对括号组合,都可以分解为:
1. 第一个左括号
2. 一个与之匹配的右括号
3. 中间有 i 对括号
4. 后面有 n-1-i 对括号
举例:
```
"(()())" 可以分解为:
( + ()() + )
↑ ↑ ↑
| | 匹配的右括号
| 中间的 i=2 对括号
第一个左括号
所以:"(())()" = "(" + "()" + ")" + "()"
i=1, n-1-i=1
```
## 代码实现
@@ -137,6 +436,8 @@ func main() {
}
```
### Go 实现(动态规划)
```go
func generateParenthesisDP(n int) []string {
if n == 0 {
@@ -161,23 +462,162 @@ func generateParenthesisDP(n int) []string {
}
```
- **时间复杂度:** O(4^n / √n)
- 在回溯树中,每个节点最多有 2 个分支
- 树的高度为 2n
- 但是由于剪枝,实际复杂度约为卡特兰数 C(n)
- 卡特兰数约为 O(4^n / (n^(3/2) * √π))
## 执行过程演示
- **空间复杂度:** O(n)
- 递归栈深度最大为 2n
- 存储结果的空间不算在内(这是必须的)
`n = 3` 为例:
### 动态规划
```
初始状态: current="", open=0, close=0
- **时间复杂度:** O(4^n / √n)
- 与回溯法类似,需要生成所有有效组合
第1层递归
current="" → 可以添加 "(" (open=0 < 3)
current="" → 不能添加 ")" (close=0 不 < open=0)
- **空间复杂度:** O(4^n / √n)
- 需要存储中间结果和最终结果
路径backtrack("(", 1, 0)
第2层递归
current="(" → 可以添加 "(" (open=1 < 3)
current="(" → 可以添加 ")" (close=0 < open=1)
路径1backtrack("((", 2, 0)
路径2backtrack("()", 1, 1)
第3层递归路径1
current="((" → 可以添加 "(" (open=2 < 3)
current="((" → 不能添加 ")" (close=0 < open=2) ✓
路径1.1backtrack("(((", 3, 0)
路径1.2backtrack("(()", 2, 1)
第4层递归路径1.1
current="(((" → 不能添加 "(" (open=3 不 < 3)
current="(((" → 不能添加 ")" (close=0 < open=3) ✓
路径1.1.1backtrack("((()", 3, 1)
第5层递归路径1.1.1
current="((()" → 不能添加 "(" (open=3 不 < 3)
current="((()" → 不能添加 ")" (close=1 < open=3) ✓
路径1.1.1.1backtrack("((())", 3, 2)
第6层递归路径1.1.1.1
current="((())" → 不能添加 "(" (open=3 不 < 3)
current="((())" → 不能添加 ")" (close=2 < open=3) ✓
路径1.1.1.1.1backtrack("((()))", 3, 3)
第7层递归路径1.1.1.1.1
current="((()))" → len=6=2*3终止
添加到结果:["((()))"]
...(继续其他路径)
最终结果:
["((()))","(()())","(())()","()(())","()()()"]
```
## 常见错误
### 错误1忘记剪枝条件
**错误写法:**
```go
func generateParenthesisWrong(n int) []string {
result := []string{}
var backtrack func(current string, length int)
backtrack = func(current string, length int) {
if length == 2*n {
result = append(result, current)
return
}
// 没有剪枝条件!
backtrack(current+"(", length+1)
backtrack(current+")", length+1)
}
backtrack("", 0)
return result
}
```
**正确写法:**
```go
func generateParenthesis(n int) []string {
result := []string{}
current := []byte{}
var backtrack func(open, close int)
backtrack = func(open, close int) {
if len(current) == 2*n {
result = append(result, string(current))
return
}
if open < n { // 剪枝条件1
current = append(current, '(')
backtrack(open+1, close)
current = current[:len(current)-1]
}
if close < open { // 剪枝条件2
current = append(current, ')')
backtrack(open, close+1)
current = current[:len(current)-1]
}
}
backtrack(0, 0)
return result
}
```
**原因:**没有剪枝会生成大量无效组合,时间复杂度爆炸。
### 错误2使用 if-else 而不是独立的 if
**错误写法:**
```go
if open < n {
backtrack(open+1, close)
} else if close < open {
backtrack(open, close+1)
}
```
**正确写法:**
```go
if open < n {
backtrack(open+1, close)
}
if close < open {
backtrack(open, close+1)
}
```
**原因:**两个条件可能同时满足,需要都尝试。
### 错误3终止条件错误
**错误写法:**
```go
if open == n && close == n { // 错误
result = append(result, string(current))
return
}
```
**正确写法:**
```go
if len(current) == 2*n { // 正确
result = append(result, string(current))
return
}
```
**原因:**虽然 `open==n && close==n` 等价于 `len(current)==2*n`,但前者更冗余。
## 进阶问题