【中等】22-括号生成 Generate Parentheses

题目重述

题目

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

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

样例

Example1:输入n=3

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

解题过程

方法一:动态规划

思路

由于考察的是生成若干数量的括号,而样例给的是3,我们自然会第一时间考虑n分别为0,1,2的情况。

  • 当n=0时,输出为空

  • 当n=1时,只有一个情况:"()"

  • 当n=2时,有两种情况:"()()"和"(())"

其实不难看出,n=3的问题,无非就是在n=2的基础上把需要新增的"("和")"插入在什么位置的问题,此时,我们联想到尝试使用动态规划方法。

动态规划算法的核心就是初始状态状态转移方程,初始状态前面已经列出,下面要考虑的就是状态转移方程的问题了。

假设当前已经有了一个对数为n-1的有效序列,且每一对括号有自己的编号,此时需要新插入一对"()",那么分为两种情况:

  1. 新插入的括号会打乱原有的编号
  2. 新插入的括号不会打乱原有的编号

无论是否打乱原有编号,将最新编号从新字符串中去除后,仍旧会得到有效的对数为n-1的字符串,所以问题就转变为如何不打乱编号插入新编号"()"了。

如果想要在已有字符串中有效插入一对括号,那这对"()"要么不会括住任何一个符号,要么括住若干对有效的括号,也就是说:

n对有效字符串 = "(" + a对有效字符串 + ")" + b对有效字符串(a+b=n-1,0≤a≤n-1)

代码

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> result[n+1];
        if(n == 0){
            return result[0];
        }
        result[0].push_back("");
        result[1].push_back("()"); // 初始状态
        for(int i = 2; i <= n; ++i){
            for(int a = 0; a <= i-1; ++a){ // a的取值范围
                int b = i-1-a;
                for(vector<string>::iterator stringa=result[a].begin(); stringa != result[a].end(); ++stringa){
                    for(vector<string>::iterator stringb=result[b].begin(); stringb != result[b].end(); ++stringb){
                        result[i].push_back("("+*stringa+")"+*stringb);
                    }
                }
            }
        }
        return result[n];
    }
};

方法二:回溯法

思路

本题目的主要目的在于有效地生成括号,自然先要考虑到有效的约束是什么:

  1. 左右括号数目相等(均为n)

  2. 对于所有的右括号都能在其前方找到对应的左括号

此时,第二条约束还可以被翻译成如下的表达:从第一位开始截止任一位置,左括号数量≥右括号数量。也就是说,字符串中每一位究竟是左括号还是右括号仅由其前方的字符情况决定,而当前位置的字符情况又决定了后续字符的赋值情况。对于这样的情景,我们很自然地选择回溯法。

此时,我们根据有效性的约束试探性对字符串的每一位进行依次赋值,成功获取到一个长度为2*n的字符串时,便达到了我们的目的。

为了满足第一条约束,我们使用两个变量leftnumrightnum用来记录当前左括号和右括号的数量,每次赋值时,都要注意使得这两个变量不能超过n;

为了满足第二条约束,我们在赋值过程中对上述leftnumrightnum两个变量进行限制,即任一位置,leftnum ≥ rightnum

代码

在代码的编写过程,仍旧是第一步要记得处理特殊情况,在n值小于1时,输出结果应为空。

// language:C++
class Solution {
public:
    vector<string> result;
    vector<string> generateParenthesis(int n) {
        if(n<1){
            return result;
        }
        backtracking("", 0, 0, n); // 回溯
        return result;
    }
    void backtracking(string curr, int leftnum, int rightnum, int n){
        if(curr.length() == 2*n){ // 找到满足条件的长度为2*n的字符串
            result.push_back(curr);
            return ;
        }
        if(leftnum < n){ // 允许赋值为(
            backtracking(curr+"(", leftnum+1, rightnum, n);
        }
        if(rightnum < leftnum){
            backtracking(curr+")", leftnum, rightnum+1, n); 
        }
    }
};

总结

  1. 回溯法作为一种试探性方法,能够解决这类按序依次尝试的问题

  2. 当问题具有一定的迭代、递进关系时,应率先考虑动态规划方法

  3. 在leetcode上提交代码之后,回溯法的运行时间为12ms,内存消耗为15.6M;而动态规划方法的运行时间为8ms,内存消耗7.5M,表现是更好的。

posted @ 2020-04-09 16:36  陌良  阅读(103)  评论(0编辑  收藏  举报