在leetCode 中涉及到回溯的题目中 有以下几个基本题目,
通过这些题目,我们可以容易的整理出回溯的基本模版
46.全排列
问题是这样的 :给定一个 没有重复数字的序列,返回其所有可能的全排列。
整体的回溯树如下图所示:
当前在红色节点上,我们选择的列表有1,3。由于2在后面,2将无法选择。那么就便变成了树的遍历
代码如下:
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
LinkedList<Integer> track = new LinkedList<>();
backtrack(track,nums);
return res;
}
public void backtrack(LinkedList<Integer> track,int[] nums){
if(track.size() == nums.length){
res.add(new ArrayList<>(track));
return ;
}
for (int i=0;i<nums.length;i++){
//判断数据
if(!track.contains(nums[i])){
track.add(nums[i]);
backtrack(track,nums);
//撤销操作
track.removeLast();
}
}
}
}
47.全排列2
这里在46的基础上,将参数从不重复元素的数组变成了一个可以包含重复元素的数组,最简单的做法是在46的基础上,将res从arrayList变成Set就可以了
这里有一个小tip:
对于重复的元素,一定要提前进行排序,这就避免了重复元素所处的位置不相邻而引起的相同数据被纳入。
39.组合总和
原题是 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 其中组合中的数字可以无限选用。
那么我们在46的基础上进行修改:
其中退出条件从trace的长度限制变成了总和为0
if(target ==0){
res.add(new ArrayList<>(list));
return;
}
别的就没有改动了
改进
我们可以添加限制条件来达到剪枝的目的,从而减少循环次数。
通过将数组进行排序之后,我们可以得出这个结构: 如果总和小于0之后,那么后续的元素就不用进行添加了,直接退出循环就可以了
代码如下
class Solution {
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,track,target,0);
return res;
}
public void backtrack(int[] candidates, LinkedList<Integer> list,int target,int i){
if(target ==0){
res.add(new ArrayList<>(list));
return;
}
if(target <0){
return;
}
for (;i<candidates.length;i++){
list.add(candidates[i]);
backtrack(candidates,list,target-candidates[i],i);
list.removeLast();
}
}
}
40 组合总和2
这一题在39的基础上,添加了一个限制:组合中的元素只能使用一次,也就是说 在每次进入backtrack
的方法,每次就将i增加一个,同时将res变成set就可以完成
优化
我们使用Set的方法来解决重复的问题,我们如果需要使用List来解决问题的话,也就是回退的时候,添加一个判断,如果有当前元素与上一个元素相同的话,就跳过
代码
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
LinkedList<Integer> tracks = new LinkedList<>();
backtrack(tracks,candidates,target,0);
return res;
}
public void backtrack(LinkedList<Integer> tracks,int[] candidates, int target,int i){
if(target==0){
res.add(new ArrayList<>(tracks));
return;
}
if(i>=candidates.length){
return;
}
for (;i<candidates.length;){
if(target-candidates[i] >=0){
tracks.add(candidates[i]);
backtrack(tracks,candidates,target-candidates[i],++i);
tracks.removeLast();
while (i>0 && i<candidates.length && candidates[i] == candidates[i-1] ){
i++;
}
}else {
break;
}
}
}
}
总结
通过上述几题,已经可以总结出一个回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择