- 添加思路推导部分,从暴力解法分析优化过程 - 增加详细的算法流程和Q&A形式的解释 - 添加执行过程演示和常见错误分析 - 完善边界条件和复杂度分析 - 保持原有的代码实现和进阶问题
1032 lines
24 KiB
Markdown
1032 lines
24 KiB
Markdown
# 括号生成 (Generate Parentheses)
|
||
|
||
## 题目描述
|
||
|
||
数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。
|
||
|
||
### 示例
|
||
|
||
**示例 1:**
|
||
```
|
||
输入:n = 3
|
||
输出:["((()))","(()())","(())()","()(())","()()()"]
|
||
```
|
||
|
||
**示例 2:**
|
||
```
|
||
输入:n = 1
|
||
输出:["()"]
|
||
```
|
||
|
||
### 约束条件
|
||
|
||
- `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 = 8,2^(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:理解有效括号的生成规则**
|
||
|
||
```python
|
||
# 规则1:左括号数量不能超过 n
|
||
if open < n:
|
||
可以添加 '('
|
||
|
||
# 规则2:右括号数量不能超过左括号数量
|
||
if close < open:
|
||
可以添加 ')'
|
||
```
|
||
|
||
**Q: 为什么右括号数量不能超过左括号数量?**
|
||
|
||
A: 因为在任何前缀中,如果右括号多于左括号,就不可能通过后续添加括号使其变成有效括号。
|
||
|
||
举例:
|
||
```
|
||
")" → 不可能变有效,因为第一个字符就是右括号
|
||
"())(" → 中间的 "()" 后面是 ")",右括号已经多余了
|
||
```
|
||
|
||
**步骤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)
|
||
```
|
||
|
||
### 边界条件分析
|
||
|
||
**边界1:n = 1**
|
||
```
|
||
输入:n = 1
|
||
输出:["()"]
|
||
过程:
|
||
"" → "(" → "()"
|
||
终止:len("()") = 2 = 2*1
|
||
```
|
||
|
||
**边界2:n = 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] = "(" + dp[i] + ")" + dp[n-1-i]
|
||
|
||
其中 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
|
||
```
|
||
|
||
## 代码实现
|
||
|
||
### Go 实现(回溯法)
|
||
|
||
```go
|
||
package main
|
||
|
||
import "fmt"
|
||
|
||
func generateParenthesis(n int) []string {
|
||
result := []string{}
|
||
current := []byte{}
|
||
|
||
var backtrack func(open, close int)
|
||
backtrack = func(open, close int) {
|
||
// 终止条件:生成了 2n 个括号
|
||
if len(current) == 2*n {
|
||
result = append(result, string(current))
|
||
return
|
||
}
|
||
|
||
// 添加左括号:左括号数量小于 n
|
||
if open < n {
|
||
current = append(current, '(')
|
||
backtrack(open+1, close)
|
||
current = current[:len(current)-1] // 回溯
|
||
}
|
||
|
||
// 添加右括号:右括号数量小于左括号数量
|
||
if close < open {
|
||
current = append(current, ')')
|
||
backtrack(open, close+1)
|
||
current = current[:len(current)-1] // 回溯
|
||
}
|
||
}
|
||
|
||
backtrack(0, 0)
|
||
return result
|
||
}
|
||
|
||
// 测试用例
|
||
func main() {
|
||
// 测试用例1
|
||
n1 := 3
|
||
fmt.Printf("输入: n = %d\n", n1)
|
||
fmt.Printf("输出: %v\n", generateParenthesis(n1))
|
||
|
||
// 测试用例2
|
||
n2 := 1
|
||
fmt.Printf("\n输入: n = %d\n", n2)
|
||
fmt.Printf("输出: %v\n", generateParenthesis(n2))
|
||
|
||
// 测试用例3
|
||
n3 := 4
|
||
fmt.Printf("\n输入: n = %d\n", n3)
|
||
result3 := generateParenthesis(n3)
|
||
fmt.Printf("输出长度: %d\n", len(result3))
|
||
fmt.Printf("输出: %v\n", result3)
|
||
|
||
// 验证卡特兰数
|
||
for i := 1; i <= 8; i++ {
|
||
fmt.Printf("n = %d, 组合数 = %d\n", i, len(generateParenthesis(i)))
|
||
}
|
||
}
|
||
```
|
||
|
||
### Go 实现(动态规划)
|
||
|
||
```go
|
||
func generateParenthesisDP(n int) []string {
|
||
if n == 0 {
|
||
return []string{""}
|
||
}
|
||
|
||
dp := make([][]string, n+1)
|
||
dp[0] = []string{""}
|
||
|
||
for i := 1; i <= n; i++ {
|
||
dp[i] = []string{}
|
||
for j := 0; j < i; j++ {
|
||
for _, left := range dp[j] {
|
||
for _, right := range dp[i-1-j] {
|
||
dp[i] = append(dp[i], "("+left+")"+right)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return dp[n]
|
||
}
|
||
```
|
||
|
||
## 执行过程演示
|
||
|
||
以 `n = 3` 为例:
|
||
|
||
```
|
||
初始状态: current="", open=0, close=0
|
||
|
||
第1层递归:
|
||
current="" → 可以添加 "(" (open=0 < 3)
|
||
current="" → 不能添加 ")" (close=0 不 < open=0)
|
||
|
||
路径:backtrack("(", 1, 0)
|
||
|
||
第2层递归:
|
||
current="(" → 可以添加 "(" (open=1 < 3)
|
||
current="(" → 可以添加 ")" (close=0 < open=1)
|
||
|
||
路径1:backtrack("((", 2, 0)
|
||
路径2:backtrack("()", 1, 1)
|
||
|
||
第3层递归(路径1):
|
||
current="((" → 可以添加 "(" (open=2 < 3)
|
||
current="((" → 不能添加 ")" (close=0 < open=2) ✓
|
||
|
||
路径1.1:backtrack("(((", 3, 0)
|
||
路径1.2:backtrack("(()", 2, 1)
|
||
|
||
第4层递归(路径1.1):
|
||
current="(((" → 不能添加 "(" (open=3 不 < 3)
|
||
current="(((" → 不能添加 ")" (close=0 < open=3) ✓
|
||
|
||
路径1.1.1:backtrack("((()", 3, 1)
|
||
|
||
第5层递归(路径1.1.1):
|
||
current="((()" → 不能添加 "(" (open=3 不 < 3)
|
||
current="((()" → 不能添加 ")" (close=1 < open=3) ✓
|
||
|
||
路径1.1.1.1:backtrack("((())", 3, 2)
|
||
|
||
第6层递归(路径1.1.1.1):
|
||
current="((())" → 不能添加 "(" (open=3 不 < 3)
|
||
current="((())" → 不能添加 ")" (close=2 < open=3) ✓
|
||
|
||
路径1.1.1.1.1:backtrack("((()))", 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`,但前者更冗余。
|
||
|
||
## 进阶问题
|
||
|
||
### Q1: 如何判断一个括号字符串是否有效?
|
||
|
||
**A:** 使用栈或者计数器。
|
||
|
||
```go
|
||
// 方法1: 使用栈
|
||
func isValid(s string) bool {
|
||
stack := []byte{}
|
||
for _, c := range []byte(s) {
|
||
if c == '(' {
|
||
stack = append(stack, c)
|
||
} else if len(stack) > 0 {
|
||
stack = stack[:len(stack)-1]
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
return len(stack) == 0
|
||
}
|
||
|
||
// 方法2: 使用计数器
|
||
func isValidSimple(s string) bool {
|
||
count := 0
|
||
for _, c := range s {
|
||
if c == '(' {
|
||
count++
|
||
} else if c == ')' {
|
||
count--
|
||
}
|
||
if count < 0 {
|
||
return false
|
||
}
|
||
}
|
||
return count == 0
|
||
}
|
||
```
|
||
|
||
### Q2: 如果有三种括号 ()、[]、{},应该如何生成?
|
||
|
||
**A:** 需要更复杂的逻辑来保证括号匹配。
|
||
|
||
```go
|
||
func generateMultipleParentheses(n int) []string {
|
||
types := []byte{'(', ')', '[', ']', '{', '}'}
|
||
result := []string{}
|
||
current := []byte{}
|
||
stack := []byte{}
|
||
|
||
var backtrack func(int)
|
||
backtrack = func(length int) {
|
||
if len(current) == 2*n {
|
||
result = append(result, string(current))
|
||
return
|
||
}
|
||
|
||
for i := 0; i < len(types); i += 2 {
|
||
// 添加左括号
|
||
if length < n {
|
||
current = append(current, types[i])
|
||
stack = append(stack, types[i])
|
||
backtrack(length + 1)
|
||
current = current[:len(current)-1]
|
||
stack = stack[:len(stack)-1]
|
||
}
|
||
}
|
||
|
||
for i := 1; i < len(types); i += 2 {
|
||
// 添加右括号:必须与栈顶匹配
|
||
if len(stack) > 0 && stack[len(stack)-1] == types[i-1] {
|
||
current = append(current, types[i])
|
||
stack = stack[:len(stack)-1]
|
||
backtrack(length)
|
||
current = current[:len(current)-1]
|
||
stack = append(stack, types[i-1])
|
||
}
|
||
}
|
||
}
|
||
|
||
backtrack(0)
|
||
return result
|
||
}
|
||
```
|
||
|
||
### Q3: 如何优化内存使用,特别是对于大的 n?
|
||
|
||
**A:** 可以使用生成器模式,逐个生成结果而不是全部存储。
|
||
|
||
```go
|
||
func generateParenthesisGenerator(n int, callback func(string)) {
|
||
current := make([]byte, 0, 2*n)
|
||
|
||
var backtrack func(open, close int)
|
||
backtrack = func(open, close int) {
|
||
if len(current) == 2*n {
|
||
callback(string(current))
|
||
return
|
||
}
|
||
|
||
if open < n {
|
||
current = append(current, '(')
|
||
backtrack(open+1, close)
|
||
current = current[:len(current)-1]
|
||
}
|
||
|
||
if close < open {
|
||
current = append(current, ')')
|
||
backtrack(open, close+1)
|
||
current = current[:len(current)-1]
|
||
}
|
||
}
|
||
|
||
backtrack(0, 0)
|
||
}
|
||
```
|
||
|
||
## P7 加分项
|
||
|
||
### 1. 深度理解:卡特兰数(Catalan Number)
|
||
|
||
**定义:**卡特兰数是组合数学中经常出现的数列,在许多计数问题中出现。
|
||
|
||
**公式:**
|
||
- C(n) = (2n)! / ((n+1)! × n!)
|
||
- C(n) = C(0)×C(n-1) + C(1)×C(n-2) + ... + C(n-1)×C(0)
|
||
|
||
**前几项:**1, 1, 2, 5, 14, 42, 132, 429, 1430, ...
|
||
|
||
**应用场景:**
|
||
1. 括号匹配问题(本题)
|
||
2. 二叉搜索树的计数
|
||
3. 出栈序列的计数
|
||
4. 路径计数(不穿过对角线)
|
||
|
||
**计算卡特兰数:**
|
||
|
||
```go
|
||
func catalanNumber(n int) int {
|
||
if n <= 1 {
|
||
return 1
|
||
}
|
||
|
||
// 动态规划计算
|
||
dp := make([]int, n+1)
|
||
dp[0], dp[1] = 1, 1
|
||
|
||
for i := 2; i <= n; i++ {
|
||
for j := 0; j < i; j++ {
|
||
dp[i] += dp[j] * dp[i-1-j]
|
||
}
|
||
}
|
||
|
||
return dp[n]
|
||
}
|
||
```
|
||
|
||
### 2. 实战扩展:通用回溯框架
|
||
|
||
**回溯法通用模板:**
|
||
|
||
```go
|
||
func backtrack(路径, 选择列表) {
|
||
if 满足结束条件 {
|
||
result = append(result, 路径)
|
||
return
|
||
}
|
||
|
||
for 选择 in 选择列表 {
|
||
// 做选择
|
||
路径.add(选择)
|
||
|
||
// 递归
|
||
backtrack(路径, 选择列表)
|
||
|
||
// 撤销选择(回溯)
|
||
路径.remove(选择)
|
||
}
|
||
}
|
||
```
|
||
|
||
**应用示例:排列问题**
|
||
|
||
```go
|
||
func permute(nums []int) [][]int {
|
||
result := [][]int{}
|
||
current := []int{}
|
||
used := make([]bool, len(nums))
|
||
|
||
var backtrack func()
|
||
backtrack = func() {
|
||
if len(current) == len(nums) {
|
||
temp := make([]int, len(current))
|
||
copy(temp, current)
|
||
result = append(result, temp)
|
||
return
|
||
}
|
||
|
||
for i := 0; i < len(nums); i++ {
|
||
if used[i] {
|
||
continue
|
||
}
|
||
|
||
// 做选择
|
||
current = append(current, nums[i])
|
||
used[i] = true
|
||
|
||
// 递归
|
||
backtrack()
|
||
|
||
// 撤销选择
|
||
current = current[:len(current)-1]
|
||
used[i] = false
|
||
}
|
||
}
|
||
|
||
backtrack()
|
||
return result
|
||
}
|
||
```
|
||
|
||
### 3. 变形题目
|
||
|
||
#### 变形1:最长有效括号
|
||
|
||
**LeetCode 32:** 给定一个只包含 '(' 和 ')' 的字符串,找出最长有效(正确闭合)括号子串的长度。
|
||
|
||
```go
|
||
func longestValidParentheses(s string) int {
|
||
maxLen := 0
|
||
stack := []int{-1} // 初始化为 -1,便于计算长度
|
||
|
||
for i, c := range s {
|
||
if c == '(' {
|
||
stack = append(stack, i)
|
||
} else {
|
||
stack = stack[:len(stack)-1]
|
||
if len(stack) == 0 {
|
||
stack = append(stack, i)
|
||
} else {
|
||
length := i - stack[len(stack)-1]
|
||
if length > maxLen {
|
||
maxLen = length
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return maxLen
|
||
}
|
||
```
|
||
|
||
#### 变形2:不同的二叉搜索树
|
||
|
||
**LeetCode 96:** 给定 n,求恰好由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种?
|
||
|
||
```go
|
||
func numTrees(n int) int {
|
||
dp := make([]int, n+1)
|
||
dp[0], dp[1] = 1, 1
|
||
|
||
for i := 2; i <= n; i++ {
|
||
for j := 1; j <= i; j++ {
|
||
dp[i] += dp[j-1] * dp[i-j]
|
||
}
|
||
}
|
||
|
||
return dp[n]
|
||
}
|
||
```
|
||
|
||
#### 变形3:括号分数
|
||
|
||
**LeetCode 856:** 给定一个平衡括号字符串 S,按下述规则计算该字符串的分数:
|
||
- `()` 得 1 分
|
||
- `AB` 得 `A + B` 分,其中 A 和 B 是平衡括号字符串
|
||
- `(A)` 得 `2 × A` 分,其中 A 是平衡括号字符串
|
||
|
||
```go
|
||
func scoreOfParentheses(s string) int {
|
||
stack := []int{0} // 栈底保存当前层的分数
|
||
|
||
for _, c := range s {
|
||
if c == '(' {
|
||
stack = append(stack, 0) // 新的一层,初始分数为 0
|
||
} else {
|
||
// 弹出当前层的分数
|
||
top := stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
|
||
// 计算分数
|
||
if top == 0 {
|
||
stack[len(stack)-1] += 1
|
||
} else {
|
||
stack[len(stack)-1] += 2 * top
|
||
}
|
||
}
|
||
}
|
||
|
||
return stack[0]
|
||
}
|
||
```
|
||
|
||
### 4. 优化技巧
|
||
|
||
#### 优化1:剪枝优化
|
||
|
||
在回溯过程中,尽早发现不可能的解并剪枝。
|
||
|
||
```go
|
||
func generateParenthesisOptimized(n int) []string {
|
||
result := []string{}
|
||
current := []byte{}
|
||
|
||
var backtrack func(open, close int)
|
||
backtrack = func(open, close int) {
|
||
// 剪枝:如果剩余的右括号太多,无法完成
|
||
if close > open {
|
||
return
|
||
}
|
||
|
||
if len(current) == 2*n {
|
||
result = append(result, string(current))
|
||
return
|
||
}
|
||
|
||
if open < n {
|
||
current = append(current, '(')
|
||
backtrack(open+1, close)
|
||
current = current[:len(current)-1]
|
||
}
|
||
|
||
if close < open {
|
||
current = append(current, ')')
|
||
backtrack(open, close+1)
|
||
current = current[:len(current)-1]
|
||
}
|
||
}
|
||
|
||
backtrack(0, 0)
|
||
return result
|
||
}
|
||
```
|
||
|
||
#### 优化2:迭代优化
|
||
|
||
使用迭代代替递归,避免栈溢出。
|
||
|
||
```go
|
||
func generateParenthesisIterative(n int) []string {
|
||
type state struct {
|
||
current string
|
||
open int
|
||
close int
|
||
}
|
||
|
||
result := []string{}
|
||
stack := []state{{"", 0, 0}}
|
||
|
||
for len(stack) > 0 {
|
||
// 弹出栈顶
|
||
s := stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
|
||
if len(s.current) == 2*n {
|
||
result = append(result, s.current)
|
||
continue
|
||
}
|
||
|
||
if s.open < n {
|
||
stack = append(stack, state{s.current + "(", s.open + 1, s.close})
|
||
}
|
||
|
||
if s.close < s.open {
|
||
stack = append(stack, state{s.current + ")", s.open, s.close + 1})
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
### 5. 实际应用场景
|
||
|
||
- **编译器:** 语法分析和表达式求值
|
||
- **代码格式化:** 自动添加括号
|
||
- **数学表达式:** 验证表达式有效性
|
||
- **数据验证:** 检查嵌套结构(如 HTML 标签)
|
||
|
||
### 6. 面试技巧
|
||
|
||
**面试官可能会问:**
|
||
1. "为什么要用回溯法而不是暴力枚举?"
|
||
2. "卡特兰数和这个问题有什么关系?"
|
||
3. "如何证明你的算法生成的所有组合都是有效的?"
|
||
|
||
**回答要点:**
|
||
1. 回溯法通过剪枝避免了无效组合的生成,效率更高
|
||
2. n 对括号的有效组合数等于第 n 个卡特兰数
|
||
3. 通过维护 `open` 和 `close` 计数器,保证了右括号永远不超过左括号
|
||
|
||
### 7. 相关题目推荐
|
||
|
||
- LeetCode 22: 括号生成(本题)
|
||
- LeetCode 17: 电话号码的字母组合
|
||
- LeetCode 32: 最长有效括号
|
||
- LeetCode 39: 组合总和
|
||
- LeetCode 46: 全排列
|
||
- LeetCode 78: 子集
|
||
- LeetCode 96: 不同的二叉搜索树
|