【LeetCode 热题 100】39. 组合总和——(解法一)选或不选

Problem: 39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。

整体思路

这段代码旨在解决一个经典的组合搜索问题:组合总和 (Combination Sum)。问题要求从一个无重复元素的正整数数组 candidates 中,找出所有和为 target 的组合。一个关键的特点是,数组中的同一个数字可以被无限制地重复使用。

该算法采用了 深度优先搜索 (DFS) 结合 回溯 (Backtracking) 的策略来系统地探索所有可能的组合。其核心逻辑步骤如下:

  1. 算法核心:递归与回溯

    • 算法的主体是一个名为 dfs 的递归函数。这个函数负责在 candidates 数组中探索所有可能的数字组合。
    • “路径”与“结果”:算法使用一个 path 列表来存储当前正在构建的组合(即从根节点到当前节点的路径),以及一个 ans 列表来存储所有已找到的、符合条件的最终组合。
  2. DFS函数设计

    • dfs(i, ..., target, ...):这个函数的设计非常关键。
      • i:代表当前递归层级可以从 candidates[i] 开始选择数字。这个参数是为了避免产生重复的组合(例如,有了 [2, 2, 3] 之后,就不再需要生成 [2, 3, 2])。通过只向后(或在原地)选择,我们保证了每个组合中的元素是按非递减顺序被选中的。
      • target:表示还需要凑齐的数值。每当选择一个数字,target 就会相应减少。
  3. 递归的终止条件(Base Cases)

    • 成功找到组合if (target == 0)。当 target 减为 0,说明当前 path 中的数字之和恰好等于原始的 target。这是一个有效的组合,因此需要将其拷贝一份new ArrayList<>(path))存入最终结果 ans 中。必须拷贝,因为 path 本身会在后续的回溯过程中被修改。
    • 路径无效if (i == n || target < 0)。当 i == n,表示已经考虑完了所有候选数字,但仍未凑齐 target;当 target < 0,表示当前组合的和已经超出了 target。这两种情况都说明当前路径是无效的,需要终止这条路的探索,直接 return
  4. 探索与回溯(核心选择逻辑)

    • dfs 函数中,对于当前候选数字 candidates[i],我们面临两个选择:
      a. 不选择 candidates[i]:直接跳过当前数字,去考虑下一个数字 candidates[i+1]。这通过递归调用 dfs(i + 1, ...) 来实现。
      b. 选择 candidates[i]
      i. 做选择:将 candidates[i] 添加到当前路径 path 中。
      ii. 继续探索:进行下一步递归调用 dfs(i, ..., target - candidates[i], ...)。注意,这里的第一个参数仍然是 i,而不是 i+1。这正是允许重复使用同一个数字的关键所在。
      iii. 撤销选择(回溯):当上述递归调用返回后(意味着所有从“选择candidates[i]”开始的路径都已探索完毕),必须将刚刚添加的 candidates[i]path 中移除 (path.removeLast())。这个“撤销”操作是回溯的精髓,它使得程序能够返回到上一个状态,去探索其他的可能性(例如,上一个递归层级的不选择分支)。

完整代码

class Solution {
    /**
     * 找出所有和为 target 的组合。candidates 中的数字可以被无限制重复使用。
     * @param candidates 无重复元素的正整数数组
     * @param target 目标和
     * @return 所有有效的组合列表
     */
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // ans: 存储所有符合条件的最终组合
        List<List<Integer>> ans = new ArrayList<>();
        // path: 存储当前正在构建的组合(路径)
        List<Integer> path = new ArrayList<>();
        // 从索引 0 开始进行深度优先搜索
        dfs(0, candidates, target, ans, path);
        return ans;
    }
    
    /**
     * 深度优先搜索(回溯)辅助函数
     * @param i 当前考虑的候选数字的索引
     * @param candidates 候选数字数组
     * @param target 剩余需要凑齐的目标和
     * @param ans 结果列表
     * @param path 当前路径
     */
    private void dfs(int i, int[] candidates, int target, List<List<Integer>> ans, List<Integer> path) {
        int n = candidates.length;
        
        // 递归终止条件 1: 索引越界或目标和变为负数,说明此路不通
        if (i == n || target < 0) {
            return;
        }
        
        // 递归终止条件 2: 成功找到一个组合
        if (target == 0) {
            // 将当前路径的一个快照(副本)加入结果集
            // 必须创建新列表,因为 path 会在回溯时被修改
            ans.add(new ArrayList<>(path));
            return;
        }

        // --- 核心选择逻辑 ---

        // 选择 1: 不使用当前的 candidates[i],直接考虑下一个数字
        // 递归地探索不包含当前数字的后续组合
        dfs(i + 1, candidates, target, ans, path);

        // 选择 2: 使用当前的 candidates[i]
        // 2a. 做出选择:将当前数字加入路径
        path.add(candidates[i]);
        // 2b. 继续探索:递归调用 dfs。
        //    - 第一个参数仍为 i,表示可以重复使用当前的 candidates[i]。
        //    - target 减去当前数字的值。
        dfs(i, candidates, target - candidates[i], ans, path);
        // 2c. 撤销选择(回溯):将刚才加入的数字移除,以便探索其他分支
        path.removeLast();
    }
}

时空复杂度

时间复杂度:O(S) 或 指数级

  1. 时间复杂度分析对于回溯问题通常比较复杂,因为它取决于搜索树的大小。
  2. Ncandidates 的数量,Ttarget
  3. 搜索树的深度最多为 T / min_val,其中 min_valcandidates 中的最小值。在最坏情况下(例如 candidates 中有1),深度可以是 T
  4. 在搜索树的每个节点,我们都可能进行分支。
  5. 因此,总的节点数(即 dfs 的调用次数)是指数级的。一个非常粗略的上界可以是 O(N^T),但这并不精确。更准确的描述是,时间复杂度与解的数量和搜索空间的大小有关。
  6. 在每个找到解的叶子节点,我们需要 O(T) 的时间来复制 path 到结果列表中。
  7. 一个更被接受的表述是,时间复杂度为 O(S),其中 S 是搜索树中节点的总数。这个 S 的值是输入 NT 的指数函数。

空间复杂度:O(T)

  1. 递归栈深度:空间复杂度的主要部分是递归调用栈的深度。在最坏的情况下,例如 candidates 中有 1,我们需要递归 T 次才能使 target 减到 0。因此,递归栈的最大深度是 O(T)
  2. path 列表path 列表存储了当前路径,其最大长度也与递归深度相关,同样是 O(T)
  3. 结果列表 ans:存储最终结果的空间不计入算法的额外辅助空间复杂度。

综合分析
算法所需的额外空间由递归栈和 path 列表决定。因此,空间复杂度为 O(T)

参考灵神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值