有效括号生成


  合法的括号匹配的问题之前已经讲解过了,现在再看一个括号生成的题目。

题目描述

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

例如,给出 n = 3,生成结果为:

[ “((()))”, “(()())”, “(())()”, “()(())”, “()()()” ]

题目分析

  既然之前已经分析过合法的括号判断,那么只要把n个左括号和n个右括号全排列,然后对排列的结果去做一个有效验证,好了,这个问题解决了。但是仔细一分析,判断一个括号是否有效,复杂度 O ( n ) O(n) ,生成2n个括号字符的全排列,时间复杂度 O ( 2 2 n ) O(2^{2n}) ,这很难办啊。所以全排列的方式行不通。

深度搜索法

  全排列里面有很多左括号和右括号数目不等的,可以直接排除了,所以浪费了很多判断。我们可以通过回溯提前把不合理的字符排除掉。但是回溯法只能是逐渐的字符串变长,也就是深度优先搜索的原理。但是我们这个时候在每一次增加括号的时候,我们保证右括号不会超过当前的左括号数目即可。还要保证最终的括号数目,左括号和右括号的数目都是n。
  根据这个思想,我们可以去写代码了,我们记录下当前左括号的数目,记录下当前右括号的数目,如果左括号大于右括号,且左括号数目小于n,此时我们深度搜索的路径包括两条,添加左括号和右括号。如果左括号数目等于n了,那么就只有一条路径,如果左括号数目等于右括号数目了且不等于n,此时只能添加左括号。如果两者数目相等,且等于n,则可以返回了,已经生成了一组合法的括号字符。整个算法就是一个深度遍历,可以采用递归来实现。

python代码
class Solution:
    def generateParenthesis(self,n):
        ans=[]
        self.max=n
        self.backtrack(ans, "", 0, 0)
        return ans
    def backtrack(self, ans, cur, left, right):
        if left==self.max:
            ans.append(cur +')' * (self.max - right))
            return
        if(left<self.max):
            self.backtrack(ans, cur +'(', left + 1, right)
        if right<left:
            self.backtrack(ans, cur +')', left, right + 1)

  可以看到,这种方式保证了左括号的数目始终多于右括号,这样生成出来的一定是合法的括号字符串。因为每次生成保证了都是合法的前缀,这个复杂度就大大的减少了。
  事实上,合法的括号数目,我们在卡特兰数的应用分析了n个括号的合法种类数就是卡特兰数列的第n项,我们大致可以根据卡特兰数的性质,分析问题的规模。我们知道卡特兰数的渐进上界是 O ( 4 n n ) O\left ( \frac{4^n}{ \sqrt{n}} \right ) ,而每一个合法的序列,我们是通过 O ( n ) O(n) 次添加字符生成出来的。

动态规划法

  既然这个问题和卡特兰数相关,那么一定可以根据卡特兰数的特点,一定可以把这个问题转换成以前所有状态的穷举。说的通俗一点就是,如果我们研究问题规模为n,那么问题规模之和为n-1的两两组合一定可以用得上,如果这句话理解不了可以直接跳到下一段。
  我们为了降低问题规模,可以考虑在问题规模为n-1的情况上加一对括号,可是这个多出来的一对括号应该往哪加。显然,我们可以把这个括号加在所有的合法匹配的括号之后。如果合法匹配数为0的时候,我们就把括号加在了最前面,就是 ( ) + d p n 1 ()+dp_{n-1} ,再者我们可以把括号加到一对合法括号后面,也是 ( d p 1 ) + d p n 2 (dp_1)+dp_{n-2} 依次类推,n-1个合法的括号总共可以分解出n-1种情况。每个情况分为两部分,前面一部分是dp_i,后面一部分是dp_n-1-i,其实也就是两部分满足下标之和为n-1就行。
  据此我们可以写出动态规划的递归式。 d p i = " ( " + d p j + " ) " + d p i j 1 ,   w h e r e   j < i dp_i="(" + dp_j + ")"+dp_{i-j-1}, \ where \ j<i 。事实上,几乎所有的卡特兰数问题都可以写成类似的递推式,对所有两两组合为n-1的情况然后穷举,并做出一定的修改。大家可以记住这个结论,当然也可以写成数学表达式,我直接写成程序的表达式即可。下面就可以根据这个表达式写代码。只需要对 d p j dp_j d p i j 1 dp_{i-j-1} 做个全排列即可。

python代码
class Solution:
    # 动态规划
    def generateParenthesis(self, n: int) -> List[str]:
        dp=[[] for i in range(n+1)]
        dp[0]=['']
        for i in range(1,n+1):
            for j in range(i):
                dp[i].extend(self.compent(dp[j],dp[i-j-1]))
        return dp[n]

    def compent(self, l, r):
    # 全排列代码,当时只写了一行,为了大家好理解,特此重写一份
        res=[]
        for s1 in l:
            for s2 in r:
                res.append('('+s1+')'+s2 )
        return res

  分析时间复杂度的时候大家需要注意一下,就是虽然这个代码写出来是4个for,但是这个代码的时间复杂度不是 O ( n 4 ) O(n^4) ,相比于上面的回溯法,其实时间复杂度的指数部分还是保持的,单是求问题规模等于n的情况,时间复杂度就已经达到了 O ( 4 n n ) O\left ( \frac{4^n}{ \sqrt{n}} \right )

原创文章 47 获赞 41 访问量 97万+

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/104983002
今日推荐