【LeetCode热题100道笔记】组合总数

题目描述

给你一个 无重复元素 的整数数组 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

思考一:递归回溯(无限选→有限选转化)

核心思路是将“元素可无限选”转化为“按target限制的有限选”

  1. 对每个元素,通过 Math.floor(target / candidates[i]) 计算其最大可选次数(选多了会超target,无需考虑),把无限选问题转为“选1~k次”的有限问题;
  2. 按数组索引顺序递归(从start开始),避免重复组合(如[2,3]和[3,2]);
  3. 用sum实时记录当前组合和,超target则剪枝,等于target则记录结果,实现高效回溯。

算法过程

  1. 初始化:结果列表result,当前组合t,当前和sum
  2. 递归入口:从索引0调用dfs(0),开始处理第一个元素;
  3. 递归逻辑(处理索引start及之后元素):
    • 剪枝/终止:sum>target直接返回;sum===target,将t副本加入result返回;
    • 遍历元素:从start遍历数组,计算当前元素最大可选次数k;
    • 枚举选次:对每个元素,枚举选1~k次的情况:
      • 累加sum、将元素重复j次加入t,递归处理下一个元素(i+1);
      • 回溯:减去sum、从t中删除j个该元素,恢复状态;
  4. 返回结果:递归结束后,result包含所有合法组合,返回即可。

时空复杂度

  • 时间复杂度:O(target²·n)(n为候选数个数)
    每个元素最大选次约为target/candidates[i](最坏O(target)),递归深度约为n,每个组合生成需O(target)时间(复制数组),总复杂度近似O(target²·n)。
  • 空间复杂度:O(target)
    递归栈深度最大为target(如全选1,需target个元素),t的最大长度也为target,额外空间为O(target)。

代码

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum = function(candidates, target) {
    const result = [];
    const n = candidates.length;
    let t = [];
    let sum = 0;

    const dfs = function(start) {
        if (sum > target) return;
        if (sum === target) {
            result.push([...t]);
            return;
        }

        for (let i = start; i < n; i++) {
            const k = Math.floor(target / candidates[i]);
            for (let j = 1; j <= k; j++) {
                sum += candidates[i] * j;
                t.push(...Array(j).fill(candidates[i]));
                dfs(i+1);
                sum -= candidates[i] * j;
                for (let l = 1; l <= j; l++) {
                    t.pop();
                }
            }
        }

    };

    dfs(0);

    return result;   
};

思考二:递归回溯(可重复选+排序剪枝)

核心思路是通过“递归索引不递增”实现元素可重复选,结合排序剪枝减少无效分支

  1. 可重复选的关键:递归调用时传入当前元素索引 i(而非 i+1),允许下一轮仍选择当前元素,同时限制从 start 开始遍历,避免生成顺序不同的重复组合(如 [2,3][3,2]);
  2. 排序剪枝:对 candidates 排序后,若当前元素与 sum 之和超过 target,后续元素更大,可直接跳出循环,减少无效递归;
  3. 实时维护 sumt(当前组合),sum 等于 target 时记录结果,回溯时恢复状态。

算法过程

  1. 初始化与排序
    • 初始化结果列表 result、当前组合 t、当前和 sum
    • candidates 升序排序,为剪枝做准备。
  2. 递归入口
    调用 dfs(0),从数组第0个元素开始处理(start=0 确保组合无顺序重复)。
  3. DFS核心逻辑(处理索引 start 及之后的元素):
    • 终止条件:若 sum === target,将 t 的副本加入 result,返回;
    • 遍历元素:从 start 遍历数组,对每个元素 num
      ① 剪枝:若 sum + num > target,后续元素更大,直接跳出循环;
      ② 选择元素:sum 累加 numnum 加入 t
      ③ 递归:调用 dfs(i)(传当前索引 i,允许重复选 num);
      ④ 回溯:sum 减去 numt 弹出 num,恢复状态。
  4. 返回结果:递归结束后,result 包含所有合法组合,返回即可。

时空复杂度分析

  • 时间复杂度O(n×2(target/minval))O(n × 2^(target/min_val))O(n×2(target/minval))

    • ncandidates 长度,min_valcandidates 中的最小元素;
    • 每个元素的最大选择次数约为 target/min_val,总递归分支数近似 2(target/minval)2^(target/min_val)2(target/minval)(每个元素“选”或“不选”,但受 target 限制);
    • 每个组合生成时需复制 t(最长长度为 target/min_val),实际操作次数可简化为 O(n × 2^(target/min_val)),排序的 O(n log n) 可忽略。
  • 空间复杂度O(target/minval)O(target/min_val)O(target/minval)

    • 递归栈深度最大为 target/min_val(如全选最小元素,需 target/min_val 次递归);
    • 当前组合 t 的最大长度也为 target/min_val,额外空间仅为递归栈和 t 的开销(结果存储不计入额外空间)。

代码

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum = function(candidates, target) {
    const result = [];
    const n = candidates.length;
    let t = [];
    let sum = 0;
    candidates.sort((a, b) => a - b);

    const dfs = function(start) {
        if (sum === target) {
            result.push([...t]);
            return;
        }

        for (let i = start; i < n; i++) {
            const num = candidates[i];
            if (sum + num > target) break;

            sum += num;
            t.push(num);
            dfs(i);
            sum -= num;
            t.pop();
        }

    };

    dfs(0);

    return result;   
};
<think>好的,我现在需要帮助用户查找LeetCode100的相关练习和解析。首先,我应该回顾用户提供的引用内容,看看有没有相关的信息可以整合。 引用[1]提到了回溯算法的一些目,比如组合、分割问、子集问等,这些可能属于100的一部分。引用[2]中的跳跃游戏和合并区间,引用[3]的汇总区间,这些都是常见的算法,可能在100中出现。 用户需要的是100的汇总和解析,所以我需要按照不同的算法分类整理目。比如分回溯、贪心、动态规划等。根据引用中的内容,可以列出每个类别下的目,比如回溯部分包括组合、分割问等,贪心有跳跃游戏,动态规划有最长回文子串。 同时,用户提到要生成相关问,我需要考虑用户可能的后续问,比如如何准备、解技巧或高频解析。需要确保回答结构清晰,分点列出,并给出每个目的链接或号,方便用户查找。 还要注意用户之前的引用内容,可能涉及到的目如组合总和、复原IP地址、子集等,这些都需要包含在相应的分类中。另外,动态规划部分引用[2]提到了最长回文子串,这也是100中的经典目。 最后,要确保回答符合格式要求,使用中文,正确应用LaTeX格式,行内公式用$...$,独立公式用$$...$$。同时,在末尾添加相关问,帮助用户深入探讨。检查是否有遗漏的目或分类,确保信息全面准确。</think>以下是根据LeetCode100整理的分类汇总及解析,结合高频型与解技巧,帮助您系统复习: --- ### 一、回溯算法 1. **组合** - [39.组合总和](https://leetcode.cn/problems/combination-sum/) 关键:允许重复选元素,剪枝优化(排序后跳过后续无效分支) - [40.组合总和II](https://leetcode.cn/problems/combination-sum-ii/) 关键:去重逻辑(同一层跳过相同元素)[^1] 2. **分割问** - [131.分割回文串](https://leetcode.cn/problems/palindrome-partitioning/) 关键:预处理回文子串(动态规划或中心扩展) - [93.复原IP地址](https://leetcode.cn/problems/restore-ip-addresses/) 关键:分段验证有效性(数值范围、前导零处理) 3. **排列问** - [46.全排列](https://leetcode.cn/problems/permutations/) 关键:标记已选元素,回溯时撤销状态 --- ### 二、贪心算法 1. [55.跳跃游戏](https://leetcode.cn/problems/jump-game/) **核心思路**:维护最大可达距离,若当前索引超过最大距离则失败[^2] ```python def canJump(nums): max_reach = 0 for i in range(len(nums)): if i > max_reach: return False max_reach = max(max_reach, i + nums[i]) return True ``` 2. [122.买卖股票的最佳时机II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) **贪心策略**:所有上升区间的利润累加 --- ### 三、动态规划 1. [5.最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) **状态定义**:$dp[i][j]$表示子串$s[i..j]$是否为回文 **转移方程**: $$dp[i][j] = (s[i] == s[j]) \ \text{且} \ dp[i+1][j-1] = \text{True}$$ 2. [70.爬楼梯](https://leetcode.cn/problems/climbing-stairs/) **状态转移**:$dp[n] = dp[n-1] + dp[n-2]$(斐波那契数列) --- ### 四、双指针 1. [11.盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) **关键**:左右指针向中间收敛,每次移动高度较小的指针 2. [15.三数之和](https://leetcode.cn/problems/3sum/) **关键**:排序后固定一个数,转化为两数之和问(需去重) --- ### 五、数据结构 1. [146.LRU缓存](https://leetcode.cn/problems/lru-cache/) **实现要点**:哈希表+双向链表(快速定位节点与调整顺序) 2. [23.合并K个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) **优化方法**:优先队列(小根堆)合并 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值