目录

1. 括号生成

1.1 题目解析

1.2 解法

1.3 代码实现

2. 组合

2.1 题目解析

2.2 解法

2.3 代码实现


1. 括号生成

https://leetcode.cn/problems/generate-parentheses/description/

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

提示:

  • 1 <= n <= 8

1.1 题目解析

题目本质
生成所有长度为 2n 的“合法括号串”。“合法”可等价为:任意前缀内 ) 的数量不超过 (,且总计 ( 与 ) 各 n 个。

常规解法
暴力枚举所有由 ( 与 ) 组成的 2n 长串(共2^{2n} 个),再逐个校验是否合法。

问题分析:
暴力会产生大量明显不合法的前缀(例如一上来就放 )),校验再滤掉,时间在生成阶段就浪费了,复杂度近似O(2^{2n})量级,不可取。

思路转折:
要高效 → 必须在生成阶段就“剪枝”。
核心约束只有两条:
i)左括号总数 ≤ n;
ii)任意时刻右括号数 ≤ 左括号数。
只在满足约束时继续向下搜索,就能只生成合法分支,避免无效枚举。这正是回溯 + 约束剪枝的用武之地,最终解的规模是第 n 个卡特兰数 Cn,复杂度与结果数量同阶,比暴力好很多。剪枝目的不一定是优化性能,也能防止产出错误数据。

1.2 解法

算法思想
• 回溯构造路径 path;
• 若 left < n 放 ( 继续;
• 若 right < left 放 ) 继续;
• 当 path.length() == 2 * n 收集答案。

i)维护成员变量:ret 结果集、path(StringBuffer)、left/right 当前已放数量、n 目标对数。

ii)终终止条件:path.length() == 2 * n 时加入结果返回。

iii)分支一:若 left < n,append('('),left++,递归,回溯时对称撤销(left-- 与删末字符)。

iv)分支二:若 right < left,append(')'),right++,递归,回溯时对称撤销(right-- 与删末字符)。

v)返回 ret。

易错点

  • 终止条件不能用 right == n,应以长度到 2n 为准。

  • 放左括号条件必须 left < n(而非 <=)。

  • 回溯要“计数器+字符”成对撤销,避免状态泄漏。

  • right < left 是前缀合法性的关键约束。

1.3 代码实现

import java.util.*;
class Solution {
    List ret;
    StringBuffer path;
    int right;
    int left;
    int n;
    public List generateParenthesis(int length) {
        ret = new ArrayList<>();
        path = new StringBuffer();
        right = 0;
        left = 0;
        n = length;
        dfs();
        return ret;
    }
    // 回溯 + 约束剪枝
    public void dfs() {
        // 长度到达 2n,说明一定 left == right == n
        if (path.length() == 2 * n) {
            ret.add(path.toString());
            return;
        }
        // 还能放左括号
        if (left < n) {
            path.append('(');
            left++;
            dfs();
            left--; // 回溯:计数器恢复
            path.deleteCharAt(path.length() - 1); // 回溯:删除最后一个字符
        }
        // 右括号必须比左括号少才能放
        if (right < left) {
            path.append(')');
            right++;
            dfs();
            right--; // 回溯:计数器恢复
            path.deleteCharAt(path.length() - 1); // 回溯:删除最后一个字符
        }
    }
}

复杂度分析

  • 时间复杂度:O(C_n)(第 n 个卡特兰数,等于结果数量同阶);

  • 空间复杂度: O(n)(递归栈与构造路径,不含结果集)。

2. 组合

https://leetcode.cn/problems/combinations/

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

2.1 题目解析

题目本质
在区间 [1, n] 内选择恰好 k 个数的所有组合(无序、无重复)。本质是“从 n 个里选 k 个”的枚举问题。

常规解法
多层 for 循环硬写枚举。适合固定很小的 kkk,但代码不可扩展。

问题分析
固定层数不适配通用 n,k。通用枚举需“逐层选择+回退”的回溯框架。若无剪枝,会在“已经选满 kkk 个后仍继续递归”或“剩余元素不足以凑满”两类分支上做无用功。

思路转折
要高效 → 用回溯 + 两类剪枝
i)长度剪枝:当 path.size()==k 立刻收集并 return,阻断后续无意义扩展;
ii)容量剪枝:当前起点 key 到 n 剩余数量必须 ≥ k - path.size(),否则当前分支不可能凑满,直接停本层循环。

2.2 解法

算法思想
• 回溯构造递增路径 path,保证不重不漏;
• 当 path.size()==k:加入答案并返回;;
• 容量剪枝:循环上界设为 n - (k - path.size()) + 1

i)设全局 ret、path、n、k,从 dfs(1) 启动。

ii)命中基:path.size()==k → ret.add(copy) → return。

iii)设本层最大起点 maxStart = n - (k - path.size()) + 1。

iv)循环 i 从 key 到 maxStart:选 i,递归 dfs(i+1),回溯移除 i。

v)结束返回 ret。

易错点

  • 命中基只 add 不 return 会导致超配递归(path 继续被塞到 k+1、k+2… 虽不入库但浪费栈帧)。

  • 忽略容量剪枝,导致在“剩余可选元素数 < 需要补齐数”时仍然空转。

  • 结果收集必须拷贝新列表,避免被后续回溯污染。

2.3 代码实现

import java.util.*;
class Solution {
    List> ret;
    List path;
    int n;
    int k;
    public List> combine(int _n, int _k) {
        ret = new ArrayList<>();
        path = new ArrayList<>();
        k = _k;
        n = _n;
        dfs(1);
        return ret;
    }
    public void dfs(int key) {
        // 长度剪枝:选满即收集并返回,阻断无意义扩展
        if (path.size() == k) {
            ret.add(new ArrayList<>(path));
            return;
        }
        // 容量剪枝:当前层起点至多到 n - (还需数量) + 1
        int maxStart = n - (k - path.size()) + 1;
        for (int i = key; i <= maxStart; i++) {
            path.add(i);
            dfs(i + 1);
            path.remove(path.size() - 1);
        }
    }
}

复杂度分析

  • 时间复杂度:Θ(C(n, k)),与答案规模同阶(剪枝降低常数因子)。

  • 空间复杂度:O(k),递归深度与路径长度(不含结果集)。