39. 组合总和

39. 组合总和

中等

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

复制代码
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

复制代码
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

复制代码
输入: candidates = [2], target = 1
输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

📝 核心笔记:组合总和 (Combination Sum)

1. 核心思想 (一句话总结)

"无限续杯:选了还能再选,不选就永远拜拜。" 因为题目允许数字重复使用 ,所以在"选当前数"的分支里,递归下标依然是 i (原地踏步),而不是 i + 1。只有决定"不选"了,才移动到下一个数。

💡 图像记忆 (自助餐):

  • 你站在一道菜(candidates[i])面前。
  • 抉择 A (选) :拿一份放盘子里,不走,继续盯着这道菜看(因为还能再拿一份)。
  • 抉择 B (不选) :摇摇头,走向下一道菜(i + 1),并且再也不回头看这道菜了。
2. 算法流程 (三步走)
  1. 触底 (Base Case)
    • left == 0:钱正好花光 -> 记录答案
    • left < 0i 越界:钱超支了 或 没菜了 -> 回退
  1. 分支一:不选 (Skip)
    • 直接跳到下个索引:dfs(i + 1, left)
  1. 分支二:选 (Pick)
    • 扣钱:left - val
    • 原地递归dfs(i, new_left) (关键点!)。
    • 回溯:把刚才拿的拿出来。
🔍 代码回忆清单 (带注释版)
复制代码
// 题目:LC 39. Combination Sum
class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(0, target, candidates, ans, path);
        return ans;
    }

    // i: 当前盯着第几个数看
    // left: 还需要凑多少钱
    private void dfs(int i, int left, int[] candidates, List<List<Integer>> ans, List<Integer> path) {
        // 1. Base Case: 成功凑出
        if (left == 0) {
            ans.add(new ArrayList<>(path)); // 📸 拍照留念
            return;
        }

        // 2. Base Case: 失败 (越界 或 减过头了)
        if (i == candidates.length || left < 0) {
            return;
        }

        // 3. 决策 A: 不选当前数 (Skip)
        // 既然不选它,那就以后永远不选它了,去下一个
        dfs(i + 1, left, candidates, ans, path);

        // 4. 决策 B: 选当前数 (Pick)
        path.add(candidates[i]);
        // ⭐️ 关键点:递归 i 而不是 i+1,表示"还可以再选它"
        dfs(i, left - candidates[i], candidates, ans, path); 
        path.remove(path.size() - 1); // 回溯
    }
}
⚡ 快速复习 CheckList (易错点)
  • \] **为什么是** **dfs(i)****不是** **dfs(i+1)****?**

    • 这是本题核心。题目说"可以无限制重复被选取"。
    • dfs(i) = 选了 2,下次还能选 2。
    • dfs(i+1) = 选了 2,下次只能选 3 (这是"子集"问题的逻辑)。
  • \] **剪枝优化 (Pruning)?**

    • 如果面试官问怎么优化,可以说:先对数组排序
    • 在"选"的分支里,如果 left - candidates[i] < 0,因为数组有序,后面的数更大,肯定更减不动,直接 returnbreak,不用再递归了。
  • \] **对比 For 循环写法?**

    • 选/不选 (你的代码):二叉树,深度可能很深(一直选同一个数)。
    • For 循环枚举 :N 叉树,每次从 i 开始循环。
    • 两种都可以,选/不选的逻辑在背包问题中更通用。
🖼️ 数字演练

输入 candidates = [2, 3], target = 5

  1. DFS(0, 5): 面对 2。
  2. Skip 2 : dfs(1, 5) -> 面对 3。
    • Skip 3 : dfs(2, 5) -> 没数了,Return。
    • Pick 3 : path=[3], left=2, dfs(1, 2) (还面对 3)。
      • Pick 3 : left=-1 -> Return。
  1. Pick 2 : path=[2], left=3, dfs(0, 3) (还面对 2)。
    • Skip 2 : dfs(1, 3) -> 面对 3。
      • Pick 3 : path=[2, 3], left=0 -> Bingo! Add [2, 3]
    • Pick 2 : path=[2, 2], left=1, dfs(0, 1) (还面对 2)。
      • ... (再选 2 就超了)

(最终结果:找到了 [2, 3])

相关推荐
Frostnova丶1 小时前
LeetCode 868. 二进制间距
算法·leetcode
QQ 31316378901 小时前
文华财经指标公式
java
nix.gnehc1 小时前
深入理解Go并发核心:GMP模型与Goroutine底层原理
开发语言·算法·golang
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于SpringBoot Vue的网络课程销售管理系统为例,包含答辩的问题和答案
java·spring boot·后端
Dylan的码园2 小时前
多线程的创建与管理
java·开发语言·多线程
心本无晴.2 小时前
RAG中的混合检索(Hybrid Search):稀疏检索与稠密检索的强强联合
人工智能·python·算法
你的论文学长2 小时前
对抗知网的 N-Gram 算法:基于语义解耦的【文本重构】与【事实性核验】架构设计
人工智能·算法·重构
WW_千谷山4_sch2 小时前
MYOJ_7788:(洛谷P3387)【模板】缩点(有关强连通分量)
c++·算法·深度优先·动态规划·图论·拓扑学
小O的算法实验室2 小时前
2026年IEEE TCYB SCI1区TOP,少即是多:一种用于大规模优化的小规模学习粒子群算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进