回溯算法总体可归为以下几个步骤:
[选择列表] [路径]
1.选择合法性判断,每道题有每道题的判断方式,如果不合法,则不能将选择列表中的选择加入路径中;
2.将选择列表中的选择加入到路径中;
3.回溯(递归过程);
4.将选择从路径中移除;
47.全排列2:给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
此题关键是选择合法性判断:首先,显而易见,如果路径中的选择已经包含接下来要做的选择,那这个选择肯定不能选,棘手的是不能包含相同的全排列,如果按照普通的全排列写法,就会有重复的全排列.解决办法是:首先对数组进行判断,并用一个布尔数组used存放这个数字是否使用过的信息,如果这个数字被使用过,肯定不能选,关键是还有一种情况,就是上一个数字与这个数字相等,那么上个数字肯定就将所有含有它的全排列都穷举了,所以这个数字肯定就不能再加入到选择列表中了,如下图所示,这就是本题的剪枝.
代码如下:
class Solution { List<List<Integer>> res=new ArrayList<>(); public List<List<Integer>> permuteUnique(int[] nums) { Arrays.sort(nums);//将数组排序. boolean[] used = new boolean[nums.length]; Arrays.fill(used, false); LinkedList<Integer> track = new LinkedList<>(); backtrack(track,nums,used); return res; } public void backtrack(LinkedList<Integer> track, int[] nums,boolean[] used){ if(track.size()==nums.length){ res.add(new LinkedList<>(track));//选择列表为空,路径中选择满了,将路径添加到结果中 return; } for (int i = 0; i < nums.length; i++) { if(used[i])continue;//路径中已经有这个选择,跳过 if(i>0&&nums[i-1]==nums[i]&&used[i-1])continue;//如上文所述 used[i]=true; //做选择 track.add(nums[i]); backtrack(track, nums, used);//回溯 used[i]=false; //撤销选择 track.removeLast(); } }}
77.组合:给定两个整数n和k,返回 1 ...n中所有可能的k个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
本题和全排列不同点在于:[1,2],[2,1]在全排列中算两种情况,而在此题中算一种情况.所以每一次回溯不能从头遍历数组,而是应该从下一个数字遍历.
class Solution { List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> combine(int n, int k) { if(k<0||k>n)return res; LinkedList<Integer> track = new LinkedList<>(); backtrack(1,n, k, track); return res; } public void backtrack(int start,int n, int k,LinkedList<Integer> track){ if(track.size()==k){ res.add(new LinkedList<>(track)); return; } for(int i=start;i<=n;i++){ if(track.contains(i))continue; track.addLast(i); backtrack(i+1,n,k,track);//从当前数字的下一个数字开始遍历 track.removeLast(); } }}
39.组合总和:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。
示例 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] ]
本题与上题不同,可以无限制重复使用数字,所以回溯起点不是下一个数字,而是当前数字.base case很容易确定:当target<0时,直接返回,当target==0时,将路径加入结果中再返回.本题还可以剪枝:当target<当前元素时,就肯定没必要继续回溯了,target肯定小于0了(为了实现此剪枝,要将数组排序)
class Solution { List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { Arrays.sort(candidates);//为了实现剪枝,将数组排序 LinkedList<Integer> track = new LinkedList<>(); backtrack(0,candidates, target, track); return res; } public void backtrack(int start,int[] candidates,int target,LinkedList<Integer> track){ if(target<0){ return; } if(target==0){ res.add(new ArrayList(track)); return; } for (int i = start; i < candidates.length&&target>=candidates[i]; i++) {//剪枝 track.addLast(candidates[i]); backtrack(i,candidates,target-candidates[i],track);//从当前数字开始回溯,还要更新
track.removeLast(); //target的值 } }}
40.组合总和II:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
**示例1:**输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]
本题与上题不同,一个数字只能用一次,所以要回溯起点是下一个数字,还有一个要点:不能有重复的组合.我们将数组排序,如果下标大于这一层递归的起点并且本数字与上一个数字相等,那么这个数字就可以被剪枝,原因是:因为上一个数字与本数字相等,那么上一个数字肯定已经把所有的组合使用过了,所以这个数字就可以跳过了.
class Solution { List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates);//排序以便剪枝 boolean[] used = new boolean[candidates.length]; Arrays.fill(used,false); LinkedList<Integer> track = new LinkedList<>(); backtrack(0,candidates, target, track,used); return res; } public void backtrack(int start,int[] candidates,int target,LinkedList<Integer> track,boolean[] used){ if(target<0){ return; } if(target==0){ res.add(new ArrayList(track)); return; } for (int i = start; i < candidates.length&&target>=candidates[i]; i++) { if(i>start&&candidates[i]==candidates[i-1])continue;//如上文所述 track.addLast(candidates[i]); backtrack(i+1,candidates,target-candidates[i],track,used);//从下一个数字开始回溯 track.removeLast(); } }}