LeetCode刷题总结---回溯(1)

138 阅读4分钟

回溯算法总体可归为以下几个步骤:

[选择列表]       [路径]

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.组合:给定两个整数nk,返回 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();        }    }}