题目

<中等> 组合总和

来源:LeetCode.

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

接下来看一下解题思路:
在看这道题之前先回顾下 回溯算法
    回溯算法思想:
回朔法主要在于: 通过枚举法,对所有可能性进行遍历。 枚举的顺序是 一条路走到底,走到底之后,退一步,再向前尝试没走过的路。直到所有路都试过。因此回朔法可以简单的理解为: 走不通就退一步的枚举法就叫回朔法。而这里回退点也叫做回朔点。

回朔关键点 通过分析发现,回朔法实现的三大技术关键点分别是:
  • 一条路走到底
  • 回退一步
  • 另寻他路
关键点的实现 那么如何才能用代码实现上述三个关键点呢?
  • for 循环
  • 递归
解释如下

for循环的作用在于另寻他路: 可以用 forforfor 循环可以实现一个路径选择器的功能,该路径选择器可以逐个选择当前节点下的所有可能往下走下去的分支路径。 例如: 像树的遍历一样,走到某个节点,该节点下可能有 iii 个节点,需要遍历这所有可能的 iii 个节点。

递归可以实现一条路走到底和回退一步: 一条路走到底: 递归意味着继续向着 forforfor 给出的路径向下走一步。 如果把递归放在 forforfor 循环内部,那么 forforfor 每一次的循环,都在给出一个路径之后,进入递归,也就继续向下走了。直到递归出口(走无可走)为止。 那么这就是一条路走到底的实现方法。 递归从递归出口出来之后,就会实现回退一步。

因此 forforfor 循环和递归配合可以实现回朔: 当递归从递归出口出来之后。上一层的 forforfor 循环就会继续执行了。而 forforfor 循环的继续执行就会给出当前节点下的下一条可行路径。而后递归调用,就顺着这条从未走过的路径又向下走一步。这就是回朔

如上的描述可以用代码总结下:
void dfs() {// 这条路走到底的条件。也是递归出口if (回朔点){// 保存该结果return;   }for () {//逐步选择当前节点下的所有可能routeif (剪枝条件) {// 剪枝前的操作// 不继续往下走了,退回上层,换个路再走return;  }// 当前路径可能是条可行路径// 保存当前数据  add();// 递归发生,继续向下走一步了。dfs();// 回朔清理remove();// 该节点下的所有路径都走完了,清理堆栈,准备下一个递归。例如弹出当前节点}
}

思路一:搜索回溯:

    对于这类寻找所有可行解的题,都可以尝试用「搜索回溯」的方法来解决。
    定义递归函数 dfs(target,combine,idx)\textit{dfs}(\textit{target}, \textit{combine}, \textit{idx})dfs(target,combine,idx) 表示当前在 candidates\textit{candidates}candidates 数组的第 idx\textit{idx}idx 位,还剩 target\textit{target}target 要组合,已经组合的列表为 combine\textit{combine}combine。递归的终止条件为 target≤0\textit{target} \le 0target≤0 或者 candidates\textit{candidates}candidates 数组被全部用完。那么在当前的函数中,每次可以选择跳过不用第 idx\textit{idx}idx 个数,即执行 dfs(target,combine,idx+1)\textit{dfs}(\textit{target}, \textit{combine}, \textit{idx} + 1)dfs(target,combine,idx+1)。也可以选择使用第 idx\textit{idx}idx 个数,即执行 dfs(target−candidates[idx],combine,idx)\textit{dfs}(\textit{target} - \textit{candidates}[\textit{idx}], \textit{combine}, \textit{idx})dfs(target−candidates[idx],combine,idx),注意到每个数字可以被无限制重复选取,因此搜索的下标仍为 idx\textit{idx}idx。

class Solution {public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> combine = new ArrayList<>();dfs(candidates, target, result, combine, 0);return result;}public void dfs(int[] candidates, int target, List<List<Integer>> result, List<Integer> combine, int idx) {// 搜索完毕直接返回if (idx == candidates.length) {return;}// 符合条件保存结果if (target == 0) {result.add(new ArrayList<>(combine));return;}// 跳过数组当前元素dfs(candidates, target, result, combine, idx + 1);if (target - candidates[idx] >= 0) {combine.add(candidates[idx]);// 因为元素可以重复使用,所以传的是 idxdfs(candidates, target - candidates[idx], result, combine, idx);combine.remove(combine.size() - 1);}}
}

思路二:回溯算法 + 剪枝:

参考回溯经典例题详解
    从数组中寻找等于目标值的所有组合,可以使用目标值一次减去数组中的值,这个值可以重复使用,如果最后值为0,则是满足条件的组合,但是根据示例来看是 不计算顺序 的。如:[2, 2, 3], [2, 3, 2], [3, 2, 2] 都算 [2, 2, 3] 一种组合。
    根据这个思路,可以画出树形图,然后编码实现。
    以输入:candidates = [2, 3, 6, 7], target = 7 为例:

说明:
  • 以 target=7target = 7target=7 为 根结点 ,创建一个分支的时 做减法
  • 每一个箭头表示:从父亲结点的数值减去边上的数值,得到孩子结点的数值。边的值就是题目中给出的 candidatecandidatecandidate 数组的每个元素的值;
  • 减到 000 或者负数的时候停止,即:结点 000 和负数结点成为叶子结点;
  • 所有从根结点到结点 000 的路径(只能从上往下,没有回路)就是题目要找的一个结果。
    这棵树有 444 个叶子结点的值 000,对应的路径列表是[[2,2,3],[2,3,2],[3,2,2],[7]][[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]][[2,2,3],[2,3,2],[3,2,2],[7]],而示例中给出的输出只有 [[7],[2,2,3]][[7], [2, 2, 3]][[7],[2,2,3]]。即:题目中要求每一个符合要求的解是 不计算顺序 的。下面我们分析为什么会产生重复。
分析重复路径产生的原因

    产生重复的原因是:在每一个结点,做减法,展开分支的时候,由于题目中说 每一个元素可以重复使用,考虑了 所有的 候选数,因此出现了重复的列表。

    遇到这一类相同元素不计算顺序的问题,在搜索的时候就需要 按某种顺序搜索
    具体的做法是:每一次搜索的时候设置 下一轮搜索的起点 beginbeginbegin,请看下图。

    从每一层的第 222 个结点开始,都不能再搜索产生同一层结点已经使用过的 candidatecandidatecandidate 里的元素。

class Solution {public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> combine = new ArrayList<>();dfs(candidates, target, result, combine, 0);return result;}public void dfs(int[] candidates, int target, List<List<Integer>> result, List<Integer> combine, int begin) {// target 为负数和 0 的时候不再产生新的孩子结点if (target < 0) {return;}// 回溯点,也是递归出口if (target == 0) {// 保存结果result.add(new ArrayList<>(combine));return;}for (int i = begin; i < candidates.length; ++i) {combine.add(candidates[i]);// 由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错dfs(candidates, target - candidates[i], result, combine, i);// 状态重置combine.remove(combine.size() - 1);}}
}
剪枝优化

    如果 target 减去一个数得到负数,那么减去一个更大的树依然是负数,同样搜索不到结果。基于这个想法,可以对输入数组进行排序,添加相关逻辑达到进一步剪枝的目的;

class Solution {public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> result = new ArrayList<>();List<Integer> combine = new ArrayList<>();// 排序是剪枝的前提Arrays.sort(candidates);dfs(candidates, target, result, combine, 0);return result;}public void dfs(int[] candidates, int target, List<List<Integer>> result, List<Integer> combine, int begin) {// 回溯点,也是递归出口if (target == 0) {// 保存结果result.add(new ArrayList<>(combine));return;}for (int i = begin; i < candidates.length; ++i) {// 剪枝条件// 前提是候选数组已经有序if(target - candidates[i] < 0) {// 不继续往下走了,退回上层,换个路再走return;}combine.add(candidates[i]);dfs(candidates, target - candidates[i], result, combine, i);combine.remove(combine.size() - 1);}}
}
什么时候使用 used 数组,什么时候使用 begin 变量
  • 排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
  • 组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。

复杂度分析

时间复杂度:O(S)O(S)O(S),其中 SSS 为所有可行解的长度之和。从分析给出的搜索树可以看出时间复杂度取决于搜索树所有叶子节点的深度之和,即所有可行解的长度之和。在这题中,很难给出一个比较紧的上界,我们知道 O(n×2n)O(n \times 2^n)O(n×2n)是一个比较松的上界,即在这份代码中,nn 个位置每次考虑选或者不选,如果符合条件,就加入答案的时间代价。但是实际运行的时候,因为不可能所有的解都满足条件,递归的时候我们还会用 $\textit{target} - \textit{candidates}[\textit{idx}] \ge 0¥ 进行剪枝,所以实际运行情况是远远小于这个上界的。

空间复杂度:O(target)O(\textit{target})O(target)。除答案数组外,空间复杂度取决于递归的栈深度,在最差情况下需要递归 O(target)O(\textit{target})O(target) 层。

LeetCode-39 - 组合总和相关推荐

  1. LeetCode 39 组合总和

    LeetCode 39 组合总和 题目描述 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合.can ...

  2. leetcode 39. 组合总和 40. 组合总和 II

    leetcode 39. 组合总和 40. 组合总和 II 组合总和 给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和 ...

  3. LeetCode 39. 组合总和(排列组合 回溯)

    1. 题目 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的数字可以无 ...

  4. leetcode - 39. 组合总和

    给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的数字可以无限制重复被选 ...

  5. leetcode 39. 组合总和

    执行用时:4 ms, 在所有 C++ 提交中击败了91.98%的用户 内存消耗:10.5 MB, 在所有 C++ 提交中提交中击败了90.06%的用户 给你一个无重复元素的整数数组 candidate ...

  6. Leetcode 39 组合总和 (每日一题 20210806)

    给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合.candidates 中的数字可以 ...

  7. leetcode 39. 组合总和 思考分析

    目录 1.题目 2.思考分析 3.未经优化代码 4.剪枝优化 1.题目 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 ...

  8. leetcode —— 39. 组合总和

    给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合.candidates 中的数字可以无限制重复被选取 ...

  9. LeetCode 39. 组合总和(回溯+剪枝)

    题目描述 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的数字可以无限 ...

  10. LeetCode 39:组合总和(Javascript 解答)

    原题目 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回 ...

最新文章

  1. 常用Linux命令总结
  2. android apk签名工具_关于keytool和jarsigner工具签名的使用小结
  3. 时间序列数据库选型——本质是列存储,B-tree索引,抑或是搜索引擎中的倒排索引...
  4. JDK1.7配置及测试
  5. 连载:阿里巴巴大数据实践—数据建模综述
  6. java添加事件监听器_Java事件监听器的四种实现方式
  7. Activity跳转
  8. nodejs从服务器返回静态文件,nodejs静态资源服务器
  9. 字符串的连接最长路径查找
  10. ROS(ROUTEROS) 端口映射
  11. 计算机软件著作权 评审,软件著作权在评职称过程中有用吗
  12. 电脑如何设置定时任务、定时执行 —— 不用Windows任务计划程序,也能轻松设定计划任务、定时任务 —— 定时执行专家
  13. mailgun_用Mailgun邮寄出去!
  14. 如何写好技术部门的年度 OKR
  15. 神经网络训练样本太少,神经网络常用训练方法
  16. 图谱实战 | 百度基于异构互联知识图谱的多模内容创作技术
  17. 背景是不规则图案css,CSS3 实现花式背景图案
  18. 第三方分享QQ QQZONE
  19. 多人审批功能简单实现
  20. android权限编辑xml大全(中英文对照)

热门文章

  1. 电动汽车简化设计,“减重瘦身”不再难
  2. 如何破解带密码保护的word文件
  3. 【Linux】环境变量和命令行参数
  4. 用 Delphi 学设计模式(一) 之 简单工厂篇 (原创)
  5. 关系模式分解为BCNF,分解过程中关系依赖集为空集问题,欢迎大家解答
  6. 微信屏蔽拼多多小红书等外链,连带屏蔽QQ音乐,连自家兄弟也不放过!
  7. 100天精通Python丨办公效率篇 —— 14、Python这些小技巧,让文件管理更加智能
  8. HTML5的特效制作的基础介绍
  9. 三分钟基础:CPU 到底是怎么认识代码的?
  10. vc++6.0打开文件闪退解决办法