22. 括号生成
问题描述
给定一个整数 n,生成所有由 n 对括号组成的有效组合。有效组合需满足:
- 左括号和右括号数量相等
- 任意位置左括号数量不少于右括号
示例:
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
算法思路
方法一:BFS(广度优先搜索)
- 使用队列存储中间状态(当前字符串、左括号数、右括号数)
- 逐层扩展,每次添加一个括号
- 当字符串长度达到
2n时,将有效组合加入结果集
方法二:回溯法(DFS)
- 核心思想:递归构建字符串,通过剪枝条件保证有效性
- 状态变量:
cur:当前构建的字符串left:已用左括号数right:已用右括号数
- 递归规则:
- 左括号数量小于
n→ 添加左括号 - 右括号数量小于左括号 → 添加右括号
- 左括号数量小于
- 终止条件:字符串长度等于
2*n
方法三:动态规划
- 定义状态:
dp[i]表示由i对括号组成的所有有效组合 - 状态转移:
dp[i] = "(" + dp[j] + ")" + dp[i-j-1]
(0 ≤ j < i,内层括号数j+ 外层括号数i-j-1=i-1) - 初始状态:
dp[0] = [""]
代码实现
方法一:BFS(广度优先搜索)
class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
if (n == 0) return result;
// 使用队列存储节点:当前字符串 + 括号计数
Queue<Pair<String, int[]>> queue = new LinkedList<>();
queue.offer(new Pair<>("", new int[]{0, 0})); // 初始状态
while (!queue.isEmpty()) {
Pair<String, int[]> pair = queue.poll();
// 当前字符串
String cur = pair.getKey();
// 左括号
int left = pair.getValue()[0];
// 右括号
int right = pair.getValue()[1];
// 终止条件:达到完整长度
if (cur.length() == 2 * n) {
result.add(cur);
continue;
}
// 添加左括号(如果允许)
if (left < n) {
queue.offer(new Pair<>(cur + "(", new int[]{left + 1, right}));
}
// 添加右括号(如果允许)
if (right < left) {
queue.offer(new Pair<>(cur + ")", new int[]{left, right + 1}));
}
}
return result;
}
// 辅助类:存储字符串和括号计数
static class Pair<K, V> {
K key;
V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
K getKey() { return key; }
V getValue() { return value; }
}
}
方法二:回溯法(DFS)
class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backtrack(result, "", 0, 0, n);
return result;
}
/**
* 回溯递归函数
*
* @param result 结果列表
* @param cur 当前构建的字符串
* @param left 已使用的左括号数
* @param right 已使用的右括号数
* @param n 括号对数
*/
private void backtrack(List<String> result, String cur, int left, int right, int n) {
// 终止条件:字符串长度达到2n
if (cur.length() == 2 * n) {
result.add(cur);
return;
}
// 左括号数量小于n → 添加左括号
if (left < n) {
backtrack(result, cur + "(", left + 1, right, n);
}
// 右括号数量小于左括号 → 添加右括号
if (right < left) {
backtrack(result, cur + ")", left, right + 1, n);
}
}
}
方法三:动态规划
class Solution {
public List<String> generateParenthesis(int n) {
// dp[i] 存储i对括号的所有有效组合
List<List<String>> dp = new ArrayList<>();
// 初始化:0对括号
List<String> dp0 = new ArrayList<>();
dp0.add("");
dp.add(dp0);
// 计算dp[1]到dp[n]
for (int i = 1; i <= n; i++) {
List<String> curList = new ArrayList<>();
// 状态转移:枚举内层括号数量j
for (int j = 0; j < i; j++) {
// 内层组合:dp[j]
for (String inner : dp.get(j)) {
// 外层组合:dp[i-j-1]
for (String outer : dp.get(i - j - 1)) {
// 组合形式:"(" + inner + ")" + outer
curList.add("(" + inner + ")" + outer);
}
}
}
dp.add(curList);
}
return dp.get(n);
}
}
算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 回溯(DFS) | O(4^n/√n) | O(n) | 直观,易理解 |
| BFS | O(4^n/√n) | O(4^n/√n) | 按层生成,避免递归栈溢出 |
| 动态规划 | O(4^n/√n) | O(4^n/√n) | 自底向上,利用子问题解 |
算法过程(n=2)
回溯法(DFS)
| 递归路径 | 当前字符串 | left | right | 操作 |
|---|---|---|---|---|
| 初始 | “” | 0 | 0 | |
| 分支1 | “(” | 1 | 0 | 添加左括号 |
| 分支1.1 | “((” | 2 | 0 | 添加左括号 |
| 分支1.1.1 | “(()” | 2 | 1 | 添加右括号 |
| 分支1.1.2 | “(())” | 2 | 2 | 添加右括号 → 加入结果 |
| 分支1.2 | “()” | 1 | 1 | 添加右括号 |
| 分支1.2.1 | “()(” | 2 | 1 | 添加左括号 |
| 分支1.2.2 | “()()” | 2 | 2 | 添加右括号 → 加入结果 |
动态规划
-
初始状态:
dp[0] = [""] -
计算 dp[1]:
j=0:"(" + dp[0] + ")" + dp[0] = "()"
→dp[1] = ["()"] -
计算 dp[2]:
j=0:"(" + dp[0] + ")" + dp[1] = "()()"j=1:"(" + dp[1] + ")" + dp[0] = "(())"
→dp[2] = ["()()", "(())"]
测试用例
public static void main(String[] args) {
Solution sol = new Solution();
// 测试用例:n=3
List<String> result = sol.generateParenthesis(3);
// 预期输出:["((()))","(()())","(())()","()(())","()()()"]
System.out.println(result);
// 边界测试:n=0
System.out.println(sol.generateParenthesis(0)); // [""]
// 边界测试:n=1
System.out.println(sol.generateParenthesis(1)); // ["()"]
}
关键点
-
回溯法(DFS)核心:
- 剪枝条件:
- 左括号不超过
n个 - 右括号不超过左括号
- 左括号不超过
- 递归终止:当字符串长度达到
2n时保存结果 - 无效组合避免:通过
right < left保证任意位置左括号不少于右括号
- 剪枝条件:
-
BFS核心:
- 使用队列按层扩展状态
- 节点存储当前字符串和括号计数
- 自动实现广度优先遍历
-
动态规划核心:
- 状态定义:
dp[i]为i对括号的有效组合 - 状态转移:
"(" + dp[j] + ")" + dp[i-j-1] - 自底向上计算,避免重复计算
- 状态定义:
-
共同约束:
- 任意位置
左括号 ≥ 右括号 - 最终左右括号数量相等
- 任意位置
常见问题
-
为什么不需要检查组合有效性?
剪枝条件right < left保证了任意位置左括号数量 ≥ 右括号数量,最终组合必然有效。 -
如何处理重复组合?
递归天然避免重复,每个位置的选择独立且符合语法规则。 -
为什么时间复杂度是 O(4^n/√n)?
解的数量是卡特兰数 C_n = (1/(n+1)) * (2n choose n) ≈ 4^n/(n√(πn))。
297

被折叠的 条评论
为什么被折叠?



