22. Generate Parentheses (括号生成)三种解法(C++ & 注释)

1. 题目描述

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例:

输入:n = 3 输出:[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]

题目链接:中文题目英文题目

2. 暴力解法(Brute Force)

2.1 解题思路

暴力解法的基本思路为:1)生成所有的组合;2)检查每个组合是否有效;

首先,我们先来解决如何生成所有的组合,基本思路为:先把所有序号(idx)赋值为"(",然后当idx = n * 2(括号组合长度),即生成一个组合;之后返回到idx - 1,将这个序号的内容赋值为")",因为改变了这个序号的内容,所以我们需要往后重复上面步骤。比如下面n = 3的例子:

开始,序号0-5全部填充"(",此时idx = 5,之后idx + 1 = n * 2 = 6,生成一个组合:

012345

之后,回到idx = 5,序号5的位置填充")",我们需要往后继续idx + 1 = 6,又生成了一个组合:

012345

idx = 5完成了,返回到idx = 4,序号4的位置填充")",往后idx + 1 = 5,序号为填充"(",往后idx + 1 = 6,完成一个组合:

012345
)(

之后再次回到idx - 1 = 5,序号5的位置填充")",往后idx + 1 = 6,完成一个组合:

012345
))

到此,完成了序号4,我们需要回到idx = 3的位置,把序号3的位置填充")",重复继续上述的步骤,即可生成所有的组合。

接下来,我们来说明一下如何判断一个组合是否为有效组合。观察有效的组合,我们可以发现:1)"("的数目 = “)“的数目;2)”(“总是出现在最左边,且至少有一个。换一种说法,如果每遇到一个”(“总计数+1,每遇到一个”)“总计数-1,即总计数必须大于等于0,因为小于零时,多了一个或多个”)”,组合一定不合法。

理解上述思路,代码就不难理解啦~

2.2 实例代码

class Solution {
    vector<string> ans;
    
    void generateAllCombinations(string& str, int idx) {
        if (idx == str.size()) 
            if (ifValid(str)) ans.push_back(str);
        else {
            str[idx] = '(';
            generateAllCombinations(str, idx + 1);
            str[idx] = ')';
            generateAllCombinations(str, idx + 1);
        }        
    }

    bool ifValid(string& str) {
        int balance = 0;
        for (char c : str) {
            if (c == '(') balance++;
            else balance--;
            if (balance < 0) return false; // 左边多了一个或数个')'
        }

        return !balance;
    }

public:
    vector<string> generateParenthesis(int n) {
        string str(2 * n, '0'); // 生成2 * n个字符为'0'的字符串
        generateAllCombinations(str, 0);
        return this->ans;
    }
};

3. 回溯法(Backtracking)

3.1 解题思路

其实2. 暴力解法里面的递归生成所有组合已经用到了回溯法,只是当时没有考虑是否合法,直接生成。现在我们来考虑一下,能不能再生成组合的时候就保证结果一定是合法的呢?

我们先来看看上面说到如何判断一个组合是否合法:

接下来,我们来说明一下如何判断一个组合是否为有效组合。观察有效的组合,我们可以发现:1)"("的数目 = “)“的数目;2)”(“总是出现在最左边,且至少有一个。换一种说法,如果每遇到一个”(“总计数+1,每遇到一个”)“总计数-1,即总计数必须大于等于0,因为小于零时,多了一个或多个”)”,组合一定不合法。

一个组合要合法,必须"(“的数目等于”)",而且左边必须有一个或多个"(",不能一开始生成")",或者在确定前面合法的情况下,往后必须先添加一个或多个"(",才能保证组合是合法的。所以我们在递归的时候引入三个标记:1)"(“的数目(open);2)”)“的数目(close);3)”("的最大数目(max);再引入一个组合字符串(str);

当str的长度(len) = max * 2 = n * 2,即生成一个合法的组合,我们添加到答案中,返回上一步即可。首先,我们只管生成"(",直到open = max,因为这个时候要满足合法组合,我们不能再添加"(",需要添加对应数目的")",然后一直添加")“到close = open,我们即完成了一个合法的组合,即len = max * 2;这时,我们生成的组合是左边有open = max个”(“的组合,那么下一步需要返回左边有open = max - 1个”(“的情况,然后继续添加合法数目的”(“和”)"。我们来看看下面n = 3例子

开始,生成open从1到max的str:

012345

这时,不能继续添加")",所以我们添加")"直到close = open,至此完成了一个合法的组合:

012345
)))

然后,返回左边有open = max -1 = 2个"(“的情况,并在后面添加一个”)",此时close = 1:

012345
)

接着,可以再添加一个"("(open +1 = max = 3),往后只能添加")",直到close = open:

012345
)))

之后,返回到open = 2的情况,之前我们添加的是"(",我们这次添加")",close + 1 = 2:

012345
))

然后,前面部分已经确定合法,所以下一步必须添加"(",open + 1 = 3,往后只能添加")",close + 1 = 3:

012345
))()

往后,重复上述的步骤即可直接用回溯法直接生成所有的合法组合,不需要额外检查是否合法。

3.2 实例代码

class Solution {
    vector<string> ans;

    void backTracking(string& str, int open, int close, int max) {
        if (str.size() == max * 2) { ans.push_back(str); return; }

        string temp1 = str + "(";
        if (open < max) backTracking(temp1, open + 1, close, max);
        string temp2 = str + ")";
        if (close < open) backTracking(temp2, open, close + 1, max);
    }

public:
    vector<string> generateParenthesis(int n) {
        string str;
        backTracking(str, 0, 0, n);
        return this->ans;
    }
};

4. 动态规划(Dynamic Programming)

4.1 解题思路

动态规划离不开把大问题拆解成小问题,我们假设这么一个函数f(n)表示n对括号下,有多少个合法的组合。

那么我们从最简单的情况n = 0开始,观察一下n = 1, n = 2, n = 3,如何用前面的结果来表示后面的结果,也就是如何用f(0), f(1), f(2)来表示f(3)的结果。

f(0) = “”
f(1) = "(“f(0)”)"f(0)
f(2) = "(“f(0)”)"f(1), "(“f(1)”)"f(0)
f(3) = "(“f(1)”)"f(2), "(“f(1)”)"f(1), "(“f(2)”)"f(0)

f(n) = "(“f(n-1)”)"f(0), "(“f(n-2)”)"f(1) … "(“f(1)”)"f(n-2), "(“f(0)”)"f(n-1)

从上面的公式来看,每个组合表示为"(“f(i)”)“f(j), i >= j,且i + j = n - 1;所以得到f(n),我们先找f(0)和f(n - 1),然后再找f(1)和f(n - 2)…然后把左右交换地放在”(“x”)“y中x和y的位置,所以这里我们用两层for循环,交替替换x和y的内容,即外层循环为f(0),内层循环为f(n - 1),即”(“f(0)”)“f(n - 1);当外层循环为f(n - 1),内层循环为f(0),即”(“f(n - 1)”)"f(0)。最后把这两层循环放在一个for循环之中,计算i从0 ~ n - 1,分别计算外层循环的f(x),和内层循环的f(y)。

当然,这种思路可以分别用迭代和递归的方法来解决。递归只需上面说到的三层循环,求f(n),递归找f(0),f(1),f(2)…f(n - 1)即可;如果是迭代,需要初始化一个存储f(n)的数组,并把f(0)保存进去,然后从f(1)开始计算,并保存对应的f(n),后面计算直接调用对应的f(n),这样就不需要递归找对应的答案了。

4.2 实例代码

4.2.1 迭代实现(Iteration)

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<vector<string>> alls(1, { "" }); // 初始化带有f(0)的存储数组

        // 从n = 1开始计算f(n),并存储到alls中
        for (int j = 1; j <= n; j++) {
            vector<string> temp;
            for (int i = 0; i < j; i++) 
                for (string& left : alls[i]) 
                    for (string& right : alls[j - 1 - i])
                        temp.push_back("(" + left + ")" + right); // "("f(n-1)")"f(0), "("f(n-2)")"f(1) ... "("f(1)")"f(n-2), "("f(0)")"f(n-1) 
            
            alls.push_back(temp);
        }

        return alls[n];
    }
};

4.2.2 递归实现(Recursion)

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> temp;

        if (!n) temp.push_back("");
        else {
            for (int i = 0; i < n; i++) {
                for (string& left : generateParenthesis(i))
                    for (string& right : generateParenthesis(n - 1 - i))
                        temp.push_back("(" + left + ")" + right); // "("f(n-1)")"f(0), "("f(n-2)")"f(1) ... "("f(1)")"f(n-2), "("f(0)")"f(n-1) 
            }
        }

        return temp;
    }
};

5. 参考资料

  1. Generate Parentheses Solutions
  2. An iterative method

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值