1. 题目
读题
https://leetcode.cn/problems/combination-sum/
你好,这是Bing。根据搜索结果,leetcode 39是一个编程题目,要求你找出一个整数数组中所有可以使数字和为目标数的不同组合¹²。
例如,如果数组是[2,3,6,7],目标数是7,
那么输出应该是[[2,2,3],[7]]。
考查点
根据搜索结果,这道题的考查点是**回溯算法**²。回溯算法是一种递归搜索的方法,可以用来找出所有可能的解。这道题要求你使用回溯算法来遍历整数数组,找出所有可以使数字和为目标数的组合。你需要注意以下几点:
- 你可以重复使用数组中的同一个数字,所以在递归时不需要改变起始位置²。
- 你需要剪枝优化,即在搜索过程中排除一些不可能的情况,比如当数字和已经超过目标数时,就不需要继续搜索了²。
- 你需要排序数组,以便在剪枝时提前判断数字和是否超过目标数²。
(1) 「leetcode」39. 组合总和【回溯算法】详解! - 知乎. https://zhuanlan.zhihu.com/p/308364408.
(2) 39. 组合总和 - 力扣(Leetcode). https://leetcode.cn/problems/combination-sum/.
(3) Combination Sum - LeetCode. https://leetcode.com/problems/combination-sum/.
2. 解法
思路
当然可以。思路是这样的:
我们要找出所有可能的组合,所以我们需要遍历数组中的每个元素,看看它是否可以加入到当前的组合中。如果可以,我们就把它加入到当前组合中,然后递归地寻找剩余的目标和。如果不可以,我们就跳过它,继续考虑下一个元素。
为了避免重复的组合,我们需要保证每次递归时,只考虑当前元素及其之后的元素,而不是从头开始。这样,我们就可以保证每个元素只出现在它之前的元素后面,而不会出现在它之前的元素前面。
为了方便回溯,我们使用一个链表来存储当前的组合,每次加入一个元素时,就把它放到链表的末尾,每次回溯时,就把它从链表的末尾移除。
当目标和为零时,说明我们找到了一个有效的组合,我们就把当前的链表转换成一个数组,并加入到结果列表中。当目标和小于零时,或者数组索引越界时,说明我们已经无法找到有效的组合了,我们就直接返回。
这样,通过不断地递归和回溯,我们就可以找出所有可能的组合。
代码的思路可以分为以下几个步骤:
- 定义两个全局变量,一个是 res,用来存储最终的结果列表,一个是 tmp,用来存储当前的组合链表。
- 定义一个公共方法 combinationSum,接收两个参数,一个是 candidates 数组,一个是 target 目标和。在这个方法中,我们初始化 res 和 tmp 为空列表,然后调用一个私有方法 dfs,传入 candidates 数组,target 目标和,和 0 作为初始的数组索引。最后返回 res 作为结果。
- 定义一个私有方法 dfs,接收三个参数,一个是 candidates 数组,一个是 target 目标和,一个是 idx 数组索引。在这个方法中,我们进行以下判断和操作:
- 如果 target 等于 0,说明我们找到了一个有效的组合,我们就把 tmp 链表转换成数组,并加入到 res 列表中,然后返回。
- 如果 idx 大于等于 candidates 数组的长度,或者 target 小于 0,说明我们已经无法找到有效的组合了,我们就直接返回。
- 否则,我们先调用 dfs 方法,传入 candidates 数组,target 目标和,和 idx + 1 作为下一个数组索引。这样相当于跳过当前的元素,不加入到当前的组合中。
- 然后,我们把当前的元素 candidates [idx] 加入到 tmp 链表的末尾,再调用 dfs 方法,传入 candidates 数组,target - candidates [idx] 作为新的目标和,和 idx 作为不变的数组索引。这样相当于选取当前的元素,并继续寻找剩余的目标和。
- 最后,我们把 tmp 链表的最后一个元素移除,相当于回溯到上一层状态。
这样,通过不断地递归和回溯,我们就可以找出所有可能的组合。
解释一下第三条。😊
第三条是定义一个私有方法 dfs,接收三个参数,一个是 candidates 数组,一个是 target 目标和,一个是 idx 数组索引。在这个方法中,我们进行以下判断和操作:
这个方法的作用是使用回溯算法,从数组的第 idx 个元素开始,寻找所有可能的组合,使得它们的和等于 target。
我们需要考虑两种情况,一种是选取当前的元素,一种是跳过当前的元素。
为了避免重复的组合,我们需要保证每次递归时,只考虑当前元素及其之后的元素,而不是从头开始。
具体的操作如下:
- 如果 target 等于 0,说明我们找到了一个有效的组合,我们就把 tmp 链表转换成数组,并加入到 res 列表中,然后返回。这是递归的终止条件。
- 如果 idx 大于等于 candidates 数组的长度,或者 target 小于 0,说明我们已经无法找到有效的组合了,我们就直接返回。这也是递归的终止条件。
- 否则,我们先调用 dfs 方法,传入 candidates 数组,target 目标和,和 idx + 1 作为下一个数组索引。这样相当于跳过当前的元素,不加入到当前的组合中。这是递归的一种分支。
- 然后,我们把当前的元素 candidates [idx] 加入到 tmp 链表的末尾,再调用 dfs 方法,传入 candidates 数组,target - candidates [idx] 作为新的目标和,和 idx 作为不变的数组索引。这样相当于选取当前的元素,并继续寻找剩余的目标和。这是递归的另一种分支。
- 最后,我们把 tmp 链表的最后一个元素移除,相当于回溯到上一层状态。这是为了恢复之前的选择,以便进行下一次选择。
这样,通过不断地递归和回溯,我们就可以找出所有可能的组合。
具体实现
class Solution { List<List<Integer>> res; // 存储结果 LinkedList<Integer> tmp; // 存储当前组合 public List<List<Integer>> combinationSum(int[] candidates, int target) { res = new ArrayList<>(); // 初始化结果列表 tmp = new LinkedList<>(); // 初始化当前组合 dfs(candidates, target, 0); // 从数组第一个元素开始回溯 return res; // 返回结果 } private void dfs(int[] candidates, int target, int idx) { if (target == 0) { // 如果目标和为零,说明找到了一个有效的组合 res.add(new ArrayList<>(tmp)); // 将当前组合加入到结果列表中 return; // 返回 } if (idx >= candidates.length || target < 0) { // 如果数组索引越界,或者目标和小于零,说明当前组合无效 return; // 返回 } // 跳过当前元素 dfs(candidates, target, idx + 1); // 选取当前元素 tmp.add(candidates[idx]); dfs(candidates, target - candidates[idx], idx); // 继续寻找剩余的目标和 tmp.removeLast(); // 回溯,移除当前元素 } }