今日题目
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
示例 1:
输入: candidates = [2,3,6,7], target = 7 输出: [[7],[2,2,3]]
示例 2:
输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1 输出: []
示例 4:
输入: candidates = [1], target = 1 输出: [[1]]
示例 5:
输入: candidates = [1], target = 2 输出: [[1,1]]
提示:
1 <= candidates.length <= 30 1 <= candidates[i] <= 200 candidate 中的每个元素都是独一无二的。 1 <= target <= 500
思路
这道题是典型的回溯思想寻找最优解的题目,所以还是尝试按照通用模板去解题
解决一个回溯问题,实际上就是一个决策树的遍历过程。只需要思考 3 个问题:
- 1、路径:也就是已经做出的选择。
- 2、选择列表:也就是你当前可以做的选择。
- 3、结束条件:也就是到达决策树底层,无法再做选择的条件。
伪代码实现方面,回溯算法的框架:
result = [] backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
这个模板的核心就是 for 循环里面的递归,在递归调用之前做选择,在递归调用之后撤销选择,特别简单。
第一步实现全排列输出:
- 1、路径:用有序链表存储
- 2、选择列表:也就是要遍历的元素数组
- 3、结束条件:链表长度达到最大的数组长度
List<List<Integer>> res = new LinkedList<>(); /* 主函数,输入一组不重复的数字,返回它们的全排列 */ List<List<Integer>> permute(int[] nums) { // 记录「路径」这里的 选择列表 即包含在nums中 LinkedList<Integer> track = new LinkedList<>(); backtrack(nums, track); return res; } // 路径:记录在 track 中 // 选择列表:nums 中的元素 // 结束条件:nums 中的元素全都在 track 中出现 void backtrack(int[] nums, LinkedList<Integer> track) { // 触发结束条件 if (track.size() == nums.length) { res.add(new LinkedList(track)); return; } for (int i = 0; i < nums.length; i++) { // 排除不合法的选择 if (track.contains(nums[i])) continue; // 做选择 track.add(nums[i]); // 进入下一层决策树 backtrack(nums, track); // 取消选择,返回上一层决策树 track.removeLast(); } }
通过上述模板代码实现后,只是实现全排列组合输出,
下一步实现,等于正整数 target的全排列:
- 1、路径:用有序链表存储
- 2、选择列表:也就是要遍历的元素数组
- 3、结束条件:链表中元素之和等于target
- 4、剪枝:元素排序后,如果当前元素大于target,后面也不会再满足,所以直接结束
public List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { LinkedList<Integer> track = new LinkedList<>(); // 排序的原因是在回溯的时候比较容易剪枝 Arrays.sort(candidates); // 套用上面的公式,candidates是指选择列表,target用来判断是否结束以及用于剪枝 // track则是路径,即已经做出的选择 backtrack(candidates, target, track); return res; } private void backtrack(int[] candidates, int target, LinkedList<Integer> track) { //先判断结束条件 if (target < 0) { return; } if (target == 0){ // 当target等于0的时候,将路径加入到结果列表中 res.add(new LinkedList<>(track)); return; } // 遍历选择列表,这里没有去重 //下面会设置start,每次递归的时候只在candidates中当前及之后的数字中选择。 for(int i = 0;i < candidates.length;i++){ // 这就是排序的好处,可以直接这样剪枝,否则还得遍历 if(target < candidates[i]){ break; } track.add(candidates[i]); backtrack(candidates,target-candidates[i],track); track.removeLast(); } }
上述代码实现了等于target的组合输出,但是并没有达到题目要求的去除重复组合的要求,输出的结果中会有重复的组合元素,所以下一步考虑去重,也就是每次不再从0开始递归元素,都是从当前节点往后进行选取,这样就不会再出现获取前面已经组合过的元素
public List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { LinkedList<Integer> track = new LinkedList<>(); Arrays.sort(candidates); backtrack(candidates, 0, target, track); return res; } private void backtrack(int[] candidates, int start, int target, LinkedList<Integer> track) { if (target < 0) { return; } if (target == 0){ res.add(new LinkedList<>(track)); return; } //int i= start去重组合的关键 for(int i = start;i < candidates.length;i++){ if(target < candidates[i]) { break; } track.add(candidates[i]); backtrack(candidates,i,target-candidates[i],track); track.removeLast(); } }
执行用时:2 ms, 在所有 Java 提交中击败了99.65%的用户
内存消耗:38.8 MB, 在所有 Java 提交中击败了21.05%的用户
小结
回溯算法解题整体都是有着类似的目的,都是在寻找所有可行解的题,我们都可以尝试用搜索回溯的方法来解决。同时要记住算法实现的基本框架,可以大大减少实现难度。