LeetCode 22:Generate Parentheses 题解(DFS / 回溯)

题目要求:给定一个整数 n,表示有 n 对括号,生成所有由这 n 对括号组成的、合法的括号字符串。合法的定义是:任意前缀中,左括号数量都不少于右括号数量,且最后左右括号数量相等。geeksforgeeks​

例子(n = 3)可能的输出之一为:"((()))", "(()())", "(())()", "()(())", "()()()"。balysnotes​


思路一:用 DFS / 回溯生成所有合法序列

这个问题非常典型:"生成所有满足某些约束的序列" → DFS + 回溯 + 剪枝。dev​

核心想法是:从空串开始,每一步可以选择加 "(" 或 ")",但要保证任何时候都不违反"括号合法"的约束。用两个计数变量来做状态:

  • open:当前已经放了多少个左括号 "("
  • close:当前已经放了多少个右括号 ")"

约束规则:

  1. 左括号数量不能超过 n:open < n 时才允许继续加 "("。algo
  2. 右括号数量不能超过左括号数量:close < open 时才允许加 ")",否则会出现前缀不合法。algo
  3. 当 open == n 且 close == n 时,说明构造出一个长度为 2 * n 的合法字符串,可以加入答案。hellointerview

固定顺序 DFS 的伪代码

先给一个常规、不随机的 DFS 伪代码,它每一层都是"先尝试加左括号,再尝试加右括号":designgurus​

text 复制代码
function generateParenthesis(n):
    result = []

    function dfs(open, close, path):
        # 如果左右括号都用完了,收集结果
        if open == n and close == n:
            result.append(path)
            return

        # 尝试加左括号 "("
        if open < n:
            dfs(open + 1, close, path + "(")

        # 尝试加右括号 ")"
        # 只有当 close < open 时才合法
        if close < open:
            dfs(open, close + 1, path + ")")

    dfs(0, 0, "")
    return result

这段代码的关键点:

  • path 是到当前为止构造出来的字符串。
  • 递归树是一棵"决策树":每一层的决策是"加 ( 还是加 )"。
  • 通过 open < n 和 close < open 这两个条件,把所有不合法路径直接剪掉了,只枚举合法前缀,所以叶子节点自然就是一整串合法括号。hellointerview

为什么固定顺序也能产生不同形状的括号串?

很多人一开始会有疑问:

明明每次都是"先试 ( 再试 )",怎么最后还能得到像 ((()))、(()())、()()() 这么不同的形状?

关键点在于:DFS 是在一棵决策树上"沿一条路走到底,然后回溯回来,换另一条路再走到底" 。algo​

以 n = 3 为例,按照上面的伪代码:

  1. 一开始从 "" 出发
  2. 先加 ( → "("
  3. 再加 ( → "(("
  4. 再加 ( → "((("
  5. 此时 open == 3 不能再加左,只能一路加右:"((()" -> "((())" -> "((()))"
  6. 得到第一条结果 "((()))"。
  7. 这条路径走完之后,递归会"退栈",回溯到上一个分叉点,比如从 "((()))" 回到 "((()" -> 再回到 "((")。
  8. 在 "((" 这个状态下,这次不再继续加左括号(因为那条路已经探索完了),而是去尝试"加右括号"的分支,走出 "(()"、"(())"、"(())()" 等路径。
  9. 继续回溯到 "(",从 "(" 出发的所有以 ( 开头的合法组合都被生成完后,再回溯回 "",尝试"根节点加右括号"这条路,但这条路一开始就不合法(右括号多于左括号),马上被剪枝,最终不会出现在结果里。

所以:

  • 每一条从根到叶的路径,就是一个不同的"加括号顺序"。
  • 虽然在每个节点都是"先试 ( 再试 )",但在不同节点试 ) 的时机不同,自然组合出了 ((()))、(()())、(())()、()(())、()()() 这几种完全不同的括号形状。finalroundai

换句话说:固定顺序 DFS 不是"只生成一种模式",而是按照固定的拓扑顺序,把整棵决策树里所有合法路径、一条一条地枚举了出来。


和显式栈思路的对应关系

一开始的直觉可能是:

写个栈,遇到 ( 入栈,遇到 ) 出栈,保证栈不为负,就能保证合法。

DFS 计数法其实就是把"栈高度"隐式存到 open - close 里:

  • 每次加 (,相当于高度 +1,也就是 open++。
  • 每次加 ),相当于高度 -1,也就是 close++。
  • 条件 close < open 等价于"当前栈高度 > 0 时才能出栈"。dev

如果偏爱显式栈,也可以这样写伪代码(思路是一样的):

text 复制代码
function generateParenthesisWithStack(n):
    result = []
    stack = []   # 用来构造当前字符串

    function dfs(open, close):
        if open == n and close == n:
            result.append(stack.join_as_string())
            return

        if open < n:
            stack.push("(")
            dfs(open + 1, close)
            stack.pop()  # 回溯

        if close < open:
            stack.push(")")
            dfs(open, close + 1)
            stack.pop()  # 回溯

    dfs(0, 0)
    return result

这里的 stack 实际上只是保存当前路径上的字符,真正的"合法性判断"仍然是通过 open 和 close 这两个计数来做的。balysnotes​


想要"顺序随机"的做法

题目本身不要求结果顺序,所以通常不需要"随机化",但如果想练习,也可以在每一层把"尝试加 ( / ) 的顺序打乱一下。sparkcodehub​

核心改动就是:

  1. 把当前能选的操作收集到一个数组里;
  2. 对这个数组做一次 shuffle;
  3. 然后按打乱后的顺序去 DFS。

伪代码示意:

text 复制代码
function generateParenthesisRandom(n):
    result = []

    function dfs(open, close, path):
        if open == n and close == n:
            result.append(path)
            return

        choices = []
        if open < n:
            choices.append("(")
        if close < open:
            choices.append(")")

        shuffle(choices)  # 打乱次序

        for ch in choices:
            if ch == "(":
                dfs(open + 1, close, path + "(")
            else:
                dfs(open, close + 1, path + ")")

    dfs(0, 0, "")
    return result

这样,每次运行时,生成的集合相同,但输出顺序可能不同,有时先看到 ((())),有时先看到 ()()(),纯属遍历顺序的差异。sparkcodehub​

相关推荐
老师我太想进步了202640 分钟前
【数据结构-1】例解算法复杂度
数据结构·算法
tyler_download1 小时前
deepseek后训练算法:位置编码算法RoPE详解1
算法
青少儿编程课堂1 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:分层图最短路解析
c++·python·算法·bfs·信息学竞赛
YSL0701242 小时前
一文通关-----排序
数据结构·算法
ZhengEnCi12 小时前
GPT-6 Astra 爆改 Qwen 权重真有那么神吗:权重空间编程 0.016% 参数的真相
算法·架构
罗西的思考12 小时前
【轻量化复刻大模型 / KIMI K3】KDA(Kimi Delta Attention):并行
人工智能·算法·机器学习
夜不会漫长14 小时前
C++:红黑树
数据结构·算法
文丑颜不良啊14 小时前
LeetCode1661~1680题目解析
java·leetcode
AI备案指南-满满15 小时前
算法备案审核不通过,一般是什么原因?
java·人工智能·算法·大模型备案·算法备案·大模型登记
Ramble_Naylor15 小时前
Rust深度解读:Send 与 Sync 的本质与线程安全边界
python·算法·rust