
1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理解。
2、对于一个「合法」的括号字符串组合 `p`,必然对于任何 `0 <= i < len(p)` 都有:子串 `p[0..i]` 中左括号的数量都大于或等于右括号的数量。
思路
明白了合法括号的性质,如何把这道题和回溯算法扯上关系呢?
算法输入一个整数 `n`,让你计算 **`n` 对儿括号**能组成几种合法的括号组合,可以改写成如下问题:
现在有 `2n` 个位置,每个位置可以放置字符 `(` 或者 `)`,组成的所有括号组合中,有多少个是合法的?
这个命题和题目的意思完全是一样的对吧,那么我们先想想如何得到全部 `2^(2n)` 种组合,然后再根据我们刚才总结出的合法括号组合的性质筛选出合法的组合,不就完事儿了?
如何得到所有的组合呢?这就是标准的暴力穷举回溯框架:
cpp
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
那么对于我们的需求,如何打印所有括号组合呢?套一下框架就出来了,伪码如下:
cpp
void backtrack(int n, int i, string& track) {
// i 代表当前的位置,共 2n 个位置
// 穷举到最后一个位置了,得到一个长度为 2n 组合
if (i == 2 * n) {
print(track);
return;
}
// 对于每个位置可以是左括号或者右括号两种选择
for choice in ['(', ')'] {
track.push(choice); // 做选择
// 穷举下一个位置
backtrack(n, i + 1, track);
track.pop(choice); // 撤销选择
}
}
那么,现在能够打印所有括号组合了,如何从它们中筛选出合法的括号组合呢?很简单,加几个条件进行「剪枝」就行了。
对于 `2n` 个位置,必然有 `n` 个左括号,`n` 个右括号,所以我们不是简单的记录穷举位置 `i`,而是用 `left` 记录还可以使用多少个左括号,用 `right` 记录还可以使用多少个右括号,这样就可以通过刚才总结的合法括号规律进行筛选了。
代码
java
public List<String> generateParenthesis(int n) {
if (n == 0) return new ArrayList<>();
// 记录所有合法的括号组合
List<String> res = new ArrayList<>();
// 回溯过程中的路径
StringBuilder track = new StringBuilder();
// 可用的左括号和右括号数量初始化为 n
backtrack(n, n, track, res);
return res;
}
// 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个
void backtrack(int left, int right,
StringBuilder track, List<String> res) {
// 若左括号剩下的多,说明不合法
if (right < left) return;
// 数量小于 0 肯定是不合法的
if (left < 0 || right < 0) return;
// 当所有括号都恰好用完时,得到一个合法的括号组合
if (left == 0 && right == 0) {
res.add(track.toString());
return;
}
// 尝试放一个左括号
track.append('('); // 选择
backtrack(left - 1, right, track, res);
track.deleteCharAt(track.length() - 1); // 撤消选择
// 尝试放一个右括号
track.append(')'); // 选择
backtrack(left, right - 1, track, res);
track.deleteCharAt(track.length() - 1); // 撤消选择
}