【Python实战】LeetCode22:生成有效的括号组合

题目描述

【Python实战】LeetCode22:生成有效的括号组合_第1张图片

解法一:暴力法

n 对括号的组合,我们可以看成是生成一个长度为 2*n 的由 “(” 或 “)” 字符构成的序列,穷举所有可能的字符序列,每个位置上可能是放入 “(” 也可能是放入 “)” ,总共有 22n 种可能,然后检测每一个是否有效。

class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        if n == 0: return [] # 0对括号返回空
        if n == 1: return ["()"] # 只有一对括号的情况
        res = []
        
        def generate(combinations):
            if len(combinations) == 2*n: # 递归终止条件,字符序列长度等于 2*n时不再添加括号
                if valid(combinations): # 检测当前的括号组合是否合法
                    res.append("".join(combinations)) 
            else:
                # 第一个位置放入一个左括号试试
                combinations.append("(") 
                generate(combinations) # 递归考虑后边 2*n -1个位置如何放
                combinations.pop() # 试完就清空序列
                # 第一个位置放入一个右括号试试
                combinations.append(")") 
                generate(combinations) # 递归考虑后边 2*n -1个位置如何放
                combinations.pop() # 试完就清空序列

        def valid(combinations):
            balance = 0 # 表示左括号的数量减去右括号的数量
            for c in combinations:          
                if c == "(":
                    balance += 1
                else:
                    balance -= 1
                if balance < 0: return False # 如果遍历到的右括号多于左括号,balance为负
            # 当左右括号一一对应时,balance最终为0
            if balance == 0: return True

        generate([])
        return res

解法二:DFS

解法一对于序列的每个位置都只是盲目地尝试两种可能的字符,等到最后填满了才判断整个序列是否合法,我们可以进一步改进,每次在添加一个新的 “(” 或 “)” 前就检测当前的放置是否合法,保证每次放入后整个括号序列仍然是有效的括号组合。

画出递归树,当作树形问题处理,就可以采用深度优先搜索求解了,其实这个解法称为“回溯”法也是可以的,只不过这里由于字符串的特殊性,Python 里 + 运算符每次都生成新的对象,每次往下面传递的时候,都是新字符串,不同于整个搜索过程都只用同一个字符串变量,因此不需要每次尝试另一种组合方式后,对字符串进行「恢复现场」和「撤销选择」的操作(“回溯”思想),具体可以参考这篇文章
【Python实战】LeetCode22:生成有效的括号组合_第2张图片
通过跟踪到目前为止已经放置的左括号和右括号的数目,即采用加分计算已使用的括号数目是否超出条件也可以。
【Python实战】LeetCode22:生成有效的括号组合_第3张图片

  • 对于左括号,只要序列中已放置的左括号数目不大于 n,就随时可以加入;
  • 对于右括号,由于每一对合法的括号,左括号必须位于右括号前面,因此,只有当已放置的右括号数目小于左括号数目时,才能放入,否则这个新加入的右括号找不到对应的左括号进行配对。
class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        if n == 0: return [] # 0对括号返回空
        if n == 1: return ["()"] # 只有一对括号的情况
        self.res = []
        self.helper(n, 0, 0, "") # 初始状态,左右括号都未放入,字符序列为空
        return self.res

    def helper(self, n, left_used, right_used, case):
        if left_used == n and right_used == n: # 递归终止条件,左右括号都用完了
            self.res.append(case)
            return
        if left_used < n: # 若左括号还有剩余,则放入一个“(”
            self.helper(n, left_used+1, right_used, case+"(")       
        if right_used < left_used: # 因为每个“)”加入时必须要有“(”配对才合法,所以先有“(”才有“)”,只有当序列中“(”多于“)”时,才能再放入一个“)”
            self.helper(n, left_used, right_used+1, case+")")

解法三:动态规划

  1. 定义状态 dp[i]:i 对括号可能生成的有效组合
  2. 状态转移方程:i 对括号的一个组合,是在 i-1 对括号的组合基础上再增加一对括号得到。而对于 i 对括号的一个组合,一定是以左括号 “(” 开头,但不一定以右括号 “)” 结尾,为此,我们可以考虑新增加的一对括号就是以这样的方式加入的,即它的左括号是放在当前整个序列的最左边,然后枚举它的右括号 “)” 可能所处的位置,从而与最左边的括号配对。
    枚举的方式就是枚举在这一对新加入的左括号和右括号中间可能存在的合法的括号对数,那么剩下的合法的括号对数就只能放在这个新加入的右括号的后面了。
    dp[i] = “(” + dp[可能的括号对数] + “)” + dp[剩下的括号对数]
  3. “可能的括号对数” 与 “剩下的括号对数” 之和得为 i - 1,“可能的括号对数” j 可以从 0 开始取到 i - 1,每取到一个 j,“剩下的括号对数”就等于 i-1-j。
    dp[i] = “(” + dp[j] + “)” + dp[i- j - 1] ,j = 0, 1, …, i - 1
class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        if n == 0:
            return []

        # 状态空间数组初始化,n对括号则对应 n种状态,每种状态dp[i]记录 i对括号可能生成的有效组合
        dp = [[] for _ in range(n + 1)] # 注意:每一个状态都是列表的形式。
        dp[0] = [""] # 0对括号的组合情况,即为空

        for i in range(1, n + 1): # 穷举 i对括号可能生成的有效组合
            cur = []
            for j in range(i): # 中间可能的括号对数 j的取值范围从 0到 i-1
                l1 = dp[j] # 放在中间的所有合法的括号组合
                l2 = dp[i - 1 - j] # 剩下的放在后边的所有合法的括号组合
                for s1 in l1:
                    for s2 in l2:
                        dp[i].append("(" + s1 + ")" + s2) # 状态转移方程
        return dp[n]

最后,如果大家有更好的Python解法,欢迎在评论区分享下您的解法,一起进步,感谢^ o ^~

你可能感兴趣的:(Python学习笔记)