一.算法题干
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
二.样例
输入:3
输出:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
三.解题思路
本来的解题思路是递归,具体思路如下。
n组括号相当于在n-1组括号的基础上,再添加一组括号,而这个括号的位置包括三种情况:第一种是"("+a+")",第二种是"()"+a,第三种是a+"()",为了防止这三种情况有重复,在前一种情况已经存在某种组合时,就不再添加后一种组合了。具体代码如下。
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
if(n==1)
{
res.push_back("()");
}
else
{
vector<string> tmpv=generateParenthesis(n-1);
for(auto i:tmpv)
{
string tmpstr1="()"+i;
res.push_back(tmpstr1);
string tmpstr2="("+i+")";
if(tmpstr2!=tmpstr1) res.push_back(tmpstr2);
string tmpstr3=i+"()";
if(tmpstr3!=tmpstr1&&tmpstr3!=tmpstr2) res.push_back(tmpstr3);
}
}
return res;
}
};
但是评测结果证明,这种做法是错误的,比如n=4时,我的输出是:
["()()()()","(()()())","()(()())","((()()))","(()())()","()()(())","(()(()))","()(())()","()((()))","(((())))","((()))()","()(())()","((())())","(())()()"]
而标准答案输出是:
["(((())))","((()()))","((())())","((()))()","(()(()))","(()()())","(()())()","(())(())","(())()()","()((()))","()(()())","()(())()","()()(())","()()()()"]
我用python对结果进行了比较。
l1=["()()()()","(()()())","()(()())","((()()))","(()())()","()()(())","(()(()))","()(())()","()((()))","(((())))","((()))()","()(())()","((())())","(())()()"]
l2=["(((())))","((()()))","((())())","((()))()","(()(()))","(()()())","(()())()","(())(())","(())()()","()((()))","()(()())","()(())()","()()(())","()()()()"]
l3=l1.copy()
for t1 in l1:
if t1 in l2:
l2.remove(t1)
l3.remove(t1)
print("我写的还剩下",l3)
print("标答还剩下",l2)
得到的结果是:
我写的还剩下 ['()(())()']
标答还剩下 ['(())(())']
而我的结果其实是重复的(也就是在结果集合中出现了2次),而答案中的情况,我给的解法并不能覆盖到(因为三种情况都不符合,即去掉一个括号后并不能构成n-1规模的子问题),所以我的答案自然是错误的。
于是我参考了这位同学的答案:【最简单易懂的】动态规划,得到了C++版本的答案。
大致思路要点包括以下几点:
- DP的两个要素是递推方程和边界条件。本题的边界条件是n=0和n=1。
- 递推方程就是从2组括号的答案开始求起,然后利用之前已经求出的答案导出之后的答案。每个答案是这样构造出来的:把对应于n组括号的答案拆分成一组括号和剩下的括号,那么这一组括号中的左括号一定在最左边,右括号会把剩余的括号分割成两部分,这两部分一定分别是规模更小的子问题且规模和为n-1,遍历即可(这里遍历的包括总的括号组数从2到n,还有被右括号分割开的两部分括号)。
四.代码实现
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
if(!n) return res;
vector<vector<string>> resv;
vector<string> tmpv;
tmpv.push_back("");
resv.push_back(tmpv);
tmpv.clear();
tmpv.push_back("()");
resv.push_back(tmpv);
for(int i=2;i<=n;++i)
{
vector<string> tmpv3;
for(int j=0;j<i;++j)
{
vector<string> tmpv1=resv[j],tmpv2=resv[i-1-j];
for(auto tmpstr1:tmpv1)
{
for(auto tmpstr2:tmpv2)
{
string tmpstr3="("+tmpstr1+")"+tmpstr2;
tmpv3.push_back(tmpstr3);
}
}
}
resv.push_back(tmpv3);
}
return resv[n];
}
};
五.对比分析
vector<string> ans;int N;
void dfs(int l,int r,string has){
if(r>l)return;
if(l > N)return;
if(l == r&& r == N){
ans.push_back(has);return;
}
dfs(l+1,r,has + "(");
dfs(l,r+1,has + ")");
}
vector<string> generateParenthesis(int n) {
N=n;if(!N)return {};
dfs(0,0,"");
return ans;
}
该思路来源于c++版本,暴力构造法+剪枝,这个倒是和我最开始递归的想法有点类似,不过他的递归思路和我不一样。
他的思路是这样的:
首先分析:需要构造有效的括号,数量上,左右括号分别都为n个。
其次:左括号的数量需要大于等于右括号的数量,
由两个前提可写出如下代码,从空串中逐步递归添加左右括号,
1.如果右括号比左括号多,说明无效则返回
2.如果左括号数量超过N,则与题意不符,返回
3.如果左右括号都达到了指定数量,则可以将其添加到数组中保存
4.如果以上条件都没有满足,则尝试加入新的左括号和右括号
真是再清楚不过了……需要借鉴的就是他的主要思想是直接搜索,只不过是去掉了绝对不合法的(也就是所谓的剪枝)。因为使用的是搜索,所以绝对不会重复。