【回溯】LC 22.括号生成

文章目录

前言

本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。

一、题目

1、原题链接

22.括号生成

2、题目描述

二、个人思路整理

1、思路分析

核心思路是回溯+剪枝

如果暴力枚举所有可能的括号组合(长度为 2 n 2n 2n,每个位置选 ()),会有 2 2 n 2^{2n} 22n 种可能,效率极低。

有效的括号序列必须满足两个关键约束:

  1. 左括号总数不能超过 n n n。
  2. 在任意前缀中,右括号数量必须小于等于左括号数量(即已放的右括号不能多于左括号,否则一定无法配对)。

因此,从空字符串开始逐步添加字符:

  • 只要 已放置的左括号数 < n n n,就可以放 (
  • 只要 已放置的右括号数 < 已放置的左括号数,就可以放 )
  • 当左、右括号数都达到 n n n(即字符串长度达到 2 n 2n 2n)时,即得到一个合法的括号组合。

2、解题代码

cpp 复制代码
class Solution {
private: 
    void backtack(int n, int left, int right, string& current, vector<string>& res) {
        // 递归终止条件:当放置的字符总长度达到 2 * n 时,说明生成了一个完整的有效组合
        if (current.size() == 2 * n) {
            res.push_back(current);
            return;
        }

        // 剪枝策略 1:只要左括号尚未用完,就可以尝试放入 '('
        if (left < n) {
            current.push_back('(');     // 做选择
            backtack(n, left + 1, right, current, res);    // 递归进入下一层
            current.pop_back();     // 撤销选择(回溯)

        }

        // 剪枝策略 2:只有当已放置的右括号少于左括号时,放入 ')' 才能保证合法配对
        if (right < left) {
            current.push_back(')');     // 做选择
            backtack(n, left, right + 1, current, res);   // 递归进入下一层
            current.pop_back();         // 撤销选择(回溯)
        }
    }
public:
    vector<string> generateParenthesis(int n) {
        vector<string> res;
        string current;
        // 初始时已使用的左、右括号数均为 0
        backtack(n, 0, 0, current, res);
        return res;
    }
};

复杂度分析

  • 时间复杂度: O ( 4 n n ) O(\frac{4^n}{\sqrt{n}}) O(n 4n)。合法组合的总数对应第 n n n 个卡特兰数 C n = 1 n + 1 ( 2 n n ) C_n = \frac{1}{n+1}\binom{2n}{n} Cn=n+11(n2n),渐近复杂度为 O ( 4 n n n ) O(\frac{4^n}{n\sqrt{n}}) O(nn 4n),生成每个合法串需要 O ( n ) O(n) O(n) 时间。
  • 空间复杂度: O ( n ) O(n) O(n)。递归调用栈的最大深度为 2 n 2n 2n(不计返回值占用的空间)。

三、知识风暴

回溯算法(Backtracking) 是本题的核心思想。它通过"做选择 → 递归 → 撤销选择"的框架,在解空间树上深度优先搜索所有可行解。理解回溯的决策树模型与剪枝思想,对掌握本题至关重要。

算法核心思想

  • 决策树建模 :本题可以抽象为一棵多叉决策树,树的深度为 2 n 2n 2n,每一层对应一个待放置的括号位置,每个节点代表"当前已构造的括号前缀"。从根到叶子节点的每一条路径,就是一个完整的括号序列。
  • 回溯本质 :深度优先遍历决策树,在进入下一层前"做选择"(放入 ()),从当前层返回时"撤销选择"(current.pop_back()),从而保证每条分支都能在共享的状态上独立探索,互不干扰。
  • 结束标记区分 :本题收集的是决策树的叶子节点,即当 current.size() == 2 * n 时收集当前 current。与子集问题"收集所有节点"不同,本题必须等左右括号都恰好用完时才收集。

常见对比:回溯 vs 暴力枚举

  • 回溯(Backtracking):通过递归 + 状态撤销,在解空间树上剪枝,避免重复计算中间状态。适合排列、组合、子集等"决策型"问题。
  • 暴力枚举(多重循环) :若括号长度固定,可用多层 for 循环直接枚举所有组合;但长度为 2 n 2n 2n 时循环层数随 n n n 变化,代码难以扩展。
  • 共同点:两者最终都能生成全部组合。区别在于回溯能显式控制每一步的选择与撤销,且代码与输入长度无关,而多重循环则受限于固定层数。

"状态撤销"标记思想

  • 核心思想 :递归进入下一层前修改共享状态(如将 () 拼入 current),返回后立即恢复原状(current.pop_back())。这样同一层的不同分支之间不会相互污染。
  • 与本题的联系 :本题用 current.push_back('(') 做选择、current.pop_back() 撤销选择,本质就是"做选择 → 递归 → 撤销选择"的体现。
  • 注意事项:撤销操作必须与选择操作严格对称。若只做选择而不撤销,会导致后续分支看到被污染的状态,从而漏解或产生错误结果。

使用要点

  • 递归终止条件 :当 current.size() == 2 * n 时,说明一条完整括号序列已经构建完毕,将其加入结果集并返回。
  • 单层遍历逻辑 :每层最多有两个分支------若 left < n 则放入 (;若 right < left 则放入 )。递归调用 backtack(n, left + 1, right, current, res)backtack(n, left, right + 1, current, res),返回后撤销选择。
  • 剪枝的对称性current.push_back(...)current.pop_back() 必须成对出现,确保恢复现场;同时利用"右括号数不能超过左括号数"这一约束提前剪枝,避免生成非法序列。
  • 结果集存储res.push_back(current) 时,容器会拷贝一份当前状态,因此后续修改 current 不会影响已保存的结果。

算法变体与扩展

  • 子集(LeetCode 78):收集决策树的所有节点,而非仅叶子节点,与本题"只收集叶子节点"形成对比。
  • 组合总和(LeetCode 39) :在决策树上做"选或不选"的剪枝,与本题"每层按约束选 ()"的框架类似。
  • 全排列(LeetCode 46):排列问题收集的是决策树的叶子节点,且每个元素必须全部选用,与本题"每层放一个括号"的框架类似。
  • 单词搜索(LeetCode 79):在网格中 DFS 匹配单词,用访问标记避免重复走格,是回溯与图遍历的结合。

相关 LeetCode 例题

相关推荐
杜 硕2 小时前
顺序表算法
数据结构·算法
AgentMaster3 小时前
智能客服多渠道接入,5 款产品的全渠道整合能力对比与实战
大数据·人工智能·算法
sali-tec4 小时前
C# 基于OpenCv的视觉工作流-章108-区域筛选
图像处理·人工智能·opencv·算法·计算机视觉
Gigavision9 小时前
基于BUAA-MIHR数据集的噪声解耦对比学习算法
人工智能·python·深度学习·算法
鹿角片ljp9 小时前
LeetCode 64:最小路径和复盘|二维 DP 与 ACM 模式完整写法
java·数据结构·算法
彧azz11 小时前
算法设计与分析:贪心与动态规划
数据结构·学习·算法·贪心算法·动态规划
Beyond_System|系统之外11 小时前
【学编程】Python基础编程题100道(21-60)
开发语言·python·算法
漂流瓶jz13 小时前
UVA-1442 洞穴 题解答案代码 算法竞赛入门经典第二版
c++·算法·题解·aoapc·算法竞赛入门经典·uva
叠层归一研究院15 小时前
基于极限自指的叠层归一宇宙结构理论——无元外部封闭系统的内生区分模型
人工智能·经验分享·算法·agi