题目介绍
力扣22题:leetcode-cn.com/problems/ge…
分析
函数签名如下:
public List<String> generateParenthesis(int n)
比如说,输入n=3,输出为如下 5 个字符串:
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
有关括号问题,你只要记住两个个性质,思路就很容易想出来:
1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个显而易见。
2、对于一个「合法」的括号字符串组合 p,必然对于任何0 <= i < len(p)都有:子串p[0..i]中左括号的数量都大于或等于右括号的数量。
如果不跟你说这个性质,可能不太容易发现,但是稍微想一下,其实是有道理的,因为从左往右算的话,肯定是左括号多嘛,到最后左右括号数量相等,说明这个括号组合是合法的。
反之,比如这个括号组合))((,前几个子串都是右括号多于左括号,显然不是合法的括号组合。
下面就来手把手实践一下回溯算法框架。
明白了合法括号的性质,如何把这道题和回溯算法扯上关系呢?
算法输入一个整数n,让你计算 n****对儿括号能组成几种合法的括号组合,可以改写成如下问题:
现在有**2n**个位置,每个位置可以放置字符 ( 或者 ) ,组成的所有括号组合中,有多少个是合法的?
这个命题和题目的意思完全是一样的对吧,那么我们先想想如何得到全部2^(2n)种组合,然后再根据我们刚才总结出的合法括号组合的性质筛选出合法的组合,不就完事儿了?
回溯算法的框架如下:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
那么对于我们的需求,如何打印所有括号组合呢?套一下框架就出来了,伪码思路如下:
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记录还可以使用多少个右括号,这样就可以通过刚才总结的合法括号规律进行筛选了:
完整代码如下:
class Solution {
public List<String> generateParenthesis(int n) {
//记录所有合法的括号组合
List<String> result = new ArrayList<>();
if(n == 0) {
return result;
}
//存储回溯过程中的路径
String track = "";
//可用的左括号和右括号的数量初始化为n,因为一共是n对括号
backTrack(n, n, track, result);
return result;
}
/**
* 可用的左括号数量为left个,可用的右括号数量为right个
*/
public void backTrack(int left, int right, String track, List<String> result) {
//若左括号剩下的多,说明不合法
if(right < left) {
return;
}
//数量小于0,肯定是不合法的
if(left < 0 || right < 0) {
return;
}
if(left == 0 && right == 0) {
result.add(track);
return;
}
/**
* 因为做选择只能选择左括号或者右括号
*/
// 尝试放一个左括号
track = track + "("; // 选择
backTrack(left - 1, right, track, result);
track = track.substring(0, track.length() - 1);// 撤消选择
// 尝试放一个右括号
track = track + ")"; // 选择
backTrack(left, right - 1, track, result);
track = track.substring(0, track.length() - 1);// 撤消选择
}
}
这样,我们的算法就完成了,借助回溯算法的框架,应该很好理解吧。