典型的DP思想(一)

一.算法题干

给出 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++版本的答案。
大致思路要点包括以下几点:

  1. DP的两个要素是递推方程和边界条件。本题的边界条件是n=0和n=1。
  2. 递推方程就是从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.如果以上条件都没有满足,则尝试加入新的左括号和右括号

真是再清楚不过了……需要借鉴的就是他的主要思想是直接搜索,只不过是去掉了绝对不合法的(也就是所谓的剪枝)。因为使用的是搜索,所以绝对不会重复。

六.题目来源

22. 括号生成

发布了22 篇原创文章 · 获赞 0 · 访问量 1283

猜你喜欢

转载自blog.csdn.net/qq_35238352/article/details/102076055