题目描述
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
解题思路
自己思路:n个括号一共有2n个“半括号”,求这2n个半括号的全排列(dfs回溯求,可以剪枝),然后一一验证。我的实现。
-
动态规划(奇技淫巧):本题最核心的思想是,考虑
i=n时相比n-1组括号增加的那一组括号的位置。当我们清楚所有
i<n时括号的可能生成排列后,对与i=n的情况,我们考虑整个括号排列中最左边的括号。
它一定是一个左括号,那么它可以和它对应的右括号组成一组完整的括号"( )",我们认为这一组是相比n-1增加进来的括号。那么,剩下n-1组括号有可能在哪呢?【这里是重点,请着重理解】
剩下的括号要么在这一组新增的括号内部,要么在这一组新增括号的外部(右侧)。
既然知道了
i<n的情况,那我们就可以对所有情况进行遍历:“(” + 【i=p时所有括号的排列组合】 + “)” + 【i=q时所有括号的排列组合】
其中
p + q = n-1,且p q均为非负整数。事实上,当上述
p从0取到n-1,q从n-1取到0后,所有情况就遍历完了。注:上述遍历是没有重复情况出现的,即当
(p1, q1) ≠ (p2, q2)时,按上述方式取的括号组合一定不同。 -
回溯(常规思路,要保证会这种思路,看参考代码2):这才是本题最可能想到的解法,上述动态规划解法不好想。回溯算法套路如下:
- 根据题目要求,画树形结构图,以便分析出递归结构;(回溯树可能有好几种画法,对应不同实现)
- 分析一个结点可以产生枝叶的条件以及可以产生哪几种枝叶、递归到哪里终止、是否可以剪枝、符合题意的结果在什么地方出现(可能在叶子结点,也可能在中间的结点);
- 完成以上两步以后,就要编写代码实现上述分析的过程,使用代码在画出的树形结构上搜索符合题意的结果。在树形结构上搜索结果集,使用的方法是执行一次“深度/广度优先遍历”。(个人偏爱dfs)
本体主要有两种思路实现:基于减法的实现和基于加法的实现。
-
基于减法的实现:(看题解的参考代码1)
如果我们使用减法,即
left表示“左括号还有几个没有用掉”,right表示“右括号还有几个没有用掉”,可以画出一棵递归树。![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trJyk2Se-1578815629623)(./imgs/20200112153701.jpg)]](https://i-blog.csdnimg.cn/blog_migrate/7fd206200463346cc4e32c848f5094dc.png)
画图以后,可以分析出的结论:
- 左右都有可以使用的括号数量,即严格大于0的时候,才产生分支;
- 左边不受右边的限制,它只受自己的约束;
- 右边除了受自己的限制以外,还受到左边的限制,即:右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以“节外生枝”;(剪枝,若不剪枝,只能在最后判断生成的括号组合是否有效)
- 在左边和右边剩余的括号数都等于0的时候结算。
-
基于加法的实现:(我的实现)
如果我们不使用减法,使用加法,即
left表示“目前用了几个左括号”,right表示“目前用了几个右括号”,那么我们可以画出一棵递归树。(以n = 2为例)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fuZ6jfiU-1578815629626)(C:\Users\F T\Desktop\面试题整理\算法\imgs\20191228160553.jpg)]](https://i-blog.csdnimg.cn/blog_migrate/11bfb6e208a030f76ff7dae723ec8168.png)
参考代码
动态规划(奇技淫巧,不推荐)
class Solution {
public:
vector<string> generateParenthesis(int n) {
if(n == 0)
return {""};
if(n == 1)
return {"()"};
vector<vector<string> > bracket(n+1); // 定义时指定vector长度为n+1
bracket[0] = {""}; // 初始化
bracket[1] = {"()"};
for(int gross = 2; gross <= n; gross++){
vector<string> temp;
for(int mid = 0; mid < gross; mid++){
int right = gross - 1 - mid;
vector<string> midVec = bracket[mid];
vector<string> rightVec = bracket[right];
for(auto midStr: midVec)
for(auto rightStr: rightVec)
temp.push_back("(" + midStr + ")" + rightStr);
}
// bracket.push_back(temp); // 此时不能用push_back!!!
bracket[gross] = temp;
}
return bracket[n];
}
};
回溯是常规解法,推荐。
回溯+剪枝(s传引用版)
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
string s;
backtrace(0, 0, n, s, res);
return res;
}
void backtrace(int left, int right, int n, string& s, vector<string>& res) {
if(left == n && right == n){
res.push_back(s);
return;
}
if(left < n){
s += "(";
backtrace(left + 1, right, n, s, res);
s.pop_back(); // 若s传的是引用,则pop_back()必须写!
}
if(right < left){ // 剪枝
s += ")";
backtrace(left, right + 1, n, s, res);
s.pop_back();
}
}
};
回溯+剪枝(s传值版)
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
string s;
backtrace(0, 0, n, s, res);
return res;
}
void backtrace(int left, int right, int n, string s, vector<string>& res) {
if(left == n && right == n){
res.push_back(s);
return;
}
if(left < n)
backtrace(left + 1, right, n, s + '(', res); // s传的不是引用
if(right < left) // 剪枝
backtrace(left, right + 1, n, s + ')', res);
// // 错误写法!
// if(left < n){
// s += "("; // 如果把 s += "(" 写在函数调用的外面,会影响下面if代码的执行
// backtrace(left + 1, right, n, s, res);
// }
//
// if(right < left){ // 剪枝
// s += ")";
// backtrace(left, right + 1, n, s, res);
// }
}
};
本文深入探讨了生成括号组合的算法,包括动态规划和回溯两种主要解法。动态规划利用已知小规模问题的解来构造大规模问题的解,而回溯则通过深度优先搜索生成所有合法括号序列。
819

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



