力扣 hot100 回溯-括号生成

组合型回溯 + 约束剪枝

一、先抓准题目本质与核心约束

1. 问题拆解

n 对括号,总长度固定是 2n,包含 n 个左括号 ( 和 n 个右括号 )。 我们要做的:在 2n 个位置里选出 n 个位置放左括号,剩下放右括号,同时保证整个字符串全程合法

2. 有效括号的两个铁则(剪枝的依据)

这是本题最核心的约束,所有不满足的分支都要直接剪掉,不继续递归:

  1. 左括号总数不能超过 n:总共只有 n 对,左括号用完了就不能再填了。
  2. 任意前缀中,右括号数量 ≤ 左括号数量 : 比如 ()) 到第三位时,右括号 2 个、左括号 1 个,已经非法了,后面无论填什么都不可能合法,直接终止这条路径。

只要全程遵守这两条规则,最终生成的字符串一定是有效的括号组合。

二、思路推导:从暴力到回溯剪枝

最笨的办法是:每个位置都可以填左或右括号,枚举所有 2^(2n) 种可能,最后再逐一判断是否有效。 但这样效率极低,很多分支中途就已经非法了。

优化思路:边填边检查,不合法就立刻停止(剪枝) 我们从左往右一位一位填,每填一位都检查是否符合上面两条规则:

  • 符合规则 → 继续递归填下一位
  • 不符合 → 直接返回,不往下走

这就是回溯的核心思想:只走合法的路,非法路径提前剪掉

三、方法一:逐位二选一(选左 / 选右)------ 对应「选或不选」思想

和组合总和的「选 / 不选」完全是同一个框架:对当前位置,我们有两个合法选择,分别递归深入,走到终点就保存答案。

1. 递归三要素

(1)递归参数
  • left:已经填了的左括号数量
  • right:已经填了的右括号数量
  • 隐含参数:当前拼接的字符串 path
(2)边界条件(终止递归)

right == n 时,说明右括号已经填够 n 个了。

为什么不用判断左括号? 因为我们全程遵守 right ≤ left,且左括号最多 n 个;当右括号到 n 时,左括号必然也等于 n,字符串刚好填完 2n 位,是合法组合。 此时把当前路径加入答案,返回。

(3)两个选择分支(对应选 / 不选)
  1. 选左括号 : 条件:left < n(左括号还没用完) 操作:路径加一个 (,左括号计数 + 1,递归下一层。

  2. 选右括号 : 条件:right < left(右括号数量不能超过左括号,保证前缀合法) 操作:路径加一个 ),右括号计数 + 1,递归下一层。

2. 恢复现场的两种写法

写法 A:push_back + pop_back(和组合总和写法完全一致)

每加一个字符递归,递归回来就删掉,恢复现场,符合你熟悉的回溯模板。

写法 B:固定长度数组直接覆盖

因为总长度固定是 2n,可以提前开一个长度为 2n 的字符串,直接按下标写入,不需要增删,自然不用显式恢复现场(新值会直接覆盖旧值)。


四、手动模拟完整过程(n=2 为例)

我们一步步走,直观感受递归和剪枝: 初始:left=0, right=0, path=""

plaintext

复制代码
dfs(0, 0, "")
├─ 能不能填左?left<2 → 可以
│  path="(", dfs(1, 0, "(")
│  ├─ 能不能填左?left<2 → 可以
│  │  path="((", dfs(2, 0, "((")
│  │  ├─ 能不能填左?left=2不满足 → 不行
│  │  └─ 能不能填右?right<left → 可以
│  │     path="(()", dfs(2, 1, "(()")
│  │     ├─ 能不能填左?不行
│  │     └─ 能不能填右?可以
│  │        path="(())", right=2 == n → 存入答案,返回
│  └─ 能不能填右?right<left → 可以
│     path="()", dfs(1, 1, "()")
│     ├─ 能不能填左?可以
│     │  path="()(", dfs(2, 1, "()(")
│     │  ├─ 填左:不行
│     │  └─ 填右:可以
│     │     path="()()", right=2 == n → 存入答案,返回
│     └─ 能不能填右?right == left → 不可以(剪枝,避免出现"())")
└─ 能不能填右?right=0, left=0 → right不小于left → 不行(剪枝,第一个字符不能是右括号)

最终得到两个合法组合:["(())", "()()"],完全正确。

cpp 复制代码
class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> ans;
        string path;


        auto dfs = [&](this auto && dfs,vector<string>&ans,string &path,int left,int right,int n){
            //left 已经填了左括号的个数 right填了右括号的个数
            //边界条件
            if(right == n){
                ans.push_back(path);
                return;
            }

            //选择1:填左括号
            if(left < n){
                path.push_back('(');
                dfs(ans,path,left+1,right,n);
                path.pop_back();
            }

            //选择2:填右括号
            if(right<left){
                path.push_back(')');
                dfs(ans,path,left,right+1,n);
                path.pop_back();
            }
        };

        dfs(ans,path,0,0,n);
        return ans;
        
    }
};