目录
1. 括号生成
https://leetcode.cn/problems/generate-parentheses/description/
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
1.1 题目解析
题目本质
生成所有长度为 2n 的“合法括号串”。“合法”可等价为:任意前缀内 ) 的数量不超过 (,且总计 ( 与 ) 各 n 个。
常规解法
暴力枚举所有由 ( 与 ) 组成的 2n 长串(共2^{2n} 个),再逐个校验是否合法。
问题分析:
暴力会产生大量明显不合法的前缀(例如一上来就放 )),校验再滤掉,时间在生成阶段就浪费了,复杂度近似O(2^{2n})量级,不可取。
思路转折:
要高效 → 必须在生成阶段就“剪枝”。
核心约束只有两条:
i)左括号总数 ≤ n;
ii)任意时刻右括号数 ≤ 左括号数。
只在满足约束时继续向下搜索,就能只生成合法分支,避免无效枚举。这正是回溯 + 约束剪枝的用武之地,最终解的规模是第 n 个卡特兰数 Cn,复杂度与结果数量同阶,比暴力好很多。剪枝目的不一定是优化性能,也能防止产出错误数据。
1.2 解法
算法思想
• 回溯构造路径 path;
• 若 left < n 放 ( 继续;
• 若 right < left 放 ) 继续;
• 当 path.length() == 2 * n 收集答案。
i)维护成员变量:ret 结果集、path(StringBuffer)、left/right 当前已放数量、n 目标对数。
ii)终终止条件:path.length() == 2 * n 时加入结果返回。
iii)分支一:若 left < n,append('('),left++,递归,回溯时对称撤销(left-- 与删末字符)。
iv)分支二:若 right < left,append(')'),right++,递归,回溯时对称撤销(right-- 与删末字符)。
v)返回 ret。
易错点
终止条件不能用 right == n,应以长度到 2n 为准。
放左括号条件必须 left < n(而非 <=)。
回溯要“计数器+字符”成对撤销,避免状态泄漏。
right < left 是前缀合法性的关键约束。
1.3 代码实现
import java.util.*;
class Solution {
List ret;
StringBuffer path;
int right;
int left;
int n;
public List generateParenthesis(int length) {
ret = new ArrayList<>();
path = new StringBuffer();
right = 0;
left = 0;
n = length;
dfs();
return ret;
}
// 回溯 + 约束剪枝
public void dfs() {
// 长度到达 2n,说明一定 left == right == n
if (path.length() == 2 * n) {
ret.add(path.toString());
return;
}
// 还能放左括号
if (left < n) {
path.append('(');
left++;
dfs();
left--; // 回溯:计数器恢复
path.deleteCharAt(path.length() - 1); // 回溯:删除最后一个字符
}
// 右括号必须比左括号少才能放
if (right < left) {
path.append(')');
right++;
dfs();
right--; // 回溯:计数器恢复
path.deleteCharAt(path.length() - 1); // 回溯:删除最后一个字符
}
}
}
复杂度分析
时间复杂度:O(C_n)(第 n 个卡特兰数,等于结果数量同阶);
空间复杂度: O(n)(递归栈与构造路径,不含结果集)。
2. 组合
https://leetcode.cn/problems/combinations/
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
1 <= n <= 201 <= k <= n
2.1 题目解析
题目本质
在区间 [1, n] 内选择恰好 k 个数的所有组合(无序、无重复)。本质是“从 n 个里选 k 个”的枚举问题。
常规解法
多层 for 循环硬写枚举。适合固定很小的 kkk,但代码不可扩展。
问题分析
固定层数不适配通用 n,k。通用枚举需“逐层选择+回退”的回溯框架。若无剪枝,会在“已经选满 kkk 个后仍继续递归”或“剩余元素不足以凑满”两类分支上做无用功。
思路转折
要高效 → 用回溯 + 两类剪枝:
i)长度剪枝:当 path.size()==k 立刻收集并 return,阻断后续无意义扩展;
ii)容量剪枝:当前起点 key 到 n 剩余数量必须 ≥ k - path.size(),否则当前分支不可能凑满,直接停本层循环。
2.2 解法
算法思想
• 回溯构造递增路径 path,保证不重不漏;
• 当 path.size()==k:加入答案并返回;;
• 容量剪枝:循环上界设为 n - (k - path.size()) + 1
i)设全局 ret、path、n、k,从 dfs(1) 启动。
ii)命中基:path.size()==k → ret.add(copy) → return。
iii)设本层最大起点 maxStart = n - (k - path.size()) + 1。
iv)循环 i 从 key 到 maxStart:选 i,递归 dfs(i+1),回溯移除 i。
v)结束返回 ret。
易错点
命中基只 add 不 return 会导致超配递归(path 继续被塞到 k+1、k+2… 虽不入库但浪费栈帧)。
忽略容量剪枝,导致在“剩余可选元素数 < 需要补齐数”时仍然空转。
结果收集必须拷贝新列表,避免被后续回溯污染。
2.3 代码实现
import java.util.*;
class Solution {
List> ret;
List path;
int n;
int k;
public List> combine(int _n, int _k) {
ret = new ArrayList<>();
path = new ArrayList<>();
k = _k;
n = _n;
dfs(1);
return ret;
}
public void dfs(int key) {
// 长度剪枝:选满即收集并返回,阻断无意义扩展
if (path.size() == k) {
ret.add(new ArrayList<>(path));
return;
}
// 容量剪枝:当前层起点至多到 n - (还需数量) + 1
int maxStart = n - (k - path.size()) + 1;
for (int i = key; i <= maxStart; i++) {
path.add(i);
dfs(i + 1);
path.remove(path.size() - 1);
}
}
}
复杂度分析
时间复杂度:Θ(C(n, k)),与答案规模同阶(剪枝降低常数因子)。
空间复杂度:O(k),递归深度与路径长度(不含结果集)。
浙公网安备 33010602011771号