一、前言
昨天说了,今天开始回溯算法的题目,今天看了两题非常经典的题目,感觉回溯的套路比动态规划更为明显。今天为什么说两题呢,因为其实47就是46的升级版而已,由无重复数字到有重复数字,我感觉一起讲更有助于理解。话不多说,看题
二、题目内容
三、题目分析
首先来看第46题,一个无重复数字的数组求可能全排列,再一看示例内容,呵!这不是排列组合嘛!nums长为3,那答案就是A33=6,没错!确实是这样。但是如果让大家写出每一种情况,可能大家会这样写(假设nums=[1,2,3]):123,132,213,231,312,321,为什么是这个顺序呢,其实就跟树一样,第一次有三个选择,在每一次选择后,剩下的选择只有nums.length-1了。
如图所示,那么这很明显是二叉树的深度遍历了。我们可以这样干,定义一个列表,然后从头开始遍历二叉树,只要列表的长度等于原数组长度,就给它保存下来,然后回到上个节点,再往另外一个子节点遍历,这样遍历一遍之后就可以保存完整的答案了。我们可以用列表类型的列表保存列表。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list=new ArrayList<List<Integer>> ();
backtrack(list,new ArrayList<>(),nums);
return list;
}
首先创建一个,Integer型列表的列表,然后进行回溯算法的方法,最后结束后返回,就可以得到答案了,仔细观察一下,backtrack中第二个参数很明显使我们用来保存每一次数据的,满足条件后将它加到list中。
现在看看backtrack该怎么写,回溯算法跟递归一样,需要一个终止点。那么如我刚刚所说,一次回溯的截止点就是当暂时列表里的数据等于原数组的长度,这时候需要将templist加到list中去,并且return,也就是返回到上一层节点去。
if(templist.size()==nums.length){
list.add(new ArrayList<>(templist));
return ;
}
但是绝大多数的时候,上面的条件是不成立的,因为此时还没有加完,那么对于这部分,应该怎么处理呢,首先,应该把当前数据加入到templist中,之后递归调用backtrack。最后一步,也是回溯不同于递归的最重要原因,就是撤销,撤销就是将原来添加的数删除,为什么呢?因为list是引用传递,当遍历到叶子节点以后要往回走,往回走的时候必须把之前添加的值给移除了,否则会越加越多。
tempList.remove(tempList.size() - 1);
所以完整代码是
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list=new ArrayList<List<Integer>> ();
backtrack(list,new ArrayList<>(),nums);
return list;
}
public void backtrack(List<List<Integer>> list,List<Integer> templist,int []nums)
{
if(templist.size()==nums.length){
list.add(new ArrayList<>(templist));
return ;
}
for(int i=0;i<nums.length;i++)
{
if(templist.contains(nums[i]))
continue;
templist.add(nums[i]);
backtrack(list,templist,nums);
templist.remove(templist.size() - 1);
}
}
至于如果数组中有重复数字,我们采取的方法是额外采用一个长度为nums.length的boolean数组,我们先将nums数组排序,相同的数肯定相邻,那在回溯时,判断的方法是:如果当前第i为数被使用过 ,就continue,如果当前数等于前一个数,并且前一个数没有被访问,那么也continue掉,那是因为同样的数留一个就够了。
这样算的话,代码应该为:
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list=new ArrayList<List<Integer>> ();
Arrays.sort(nums);
boolean []used=new boolean[nums.length];
backtrack(list,used,new ArrayList<>(),nums);
return list;
}
public void backtrack(List<List<Integer>> list,boolean []used,List<Integer> templist,int[]nums)
{
if(templist.size()==nums.length){
list.add(new ArrayList<>(templist));
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;
templist.add(nums[i]);
backtrack(list,used,templist,nums);
used[i]=false;
templist.remove(templist.size()-1);
}
}
四、感言
回溯算法比动态规划好玩多了
明天再来