leetcode47. 全排列 II 回溯剪枝的细节问题

题目描述:

leetcode47. 全排列 II 回溯剪枝的细节问题_第1张图片

 1、思路

        作为回溯算法的经典问题,常用的方法是,每次dfs前先判断是否达到临界条件,满足条件则加入结果集并return。通过循环和dfs来构建树,查找出全部满足条件的集合。
        例如本题,如1,2,3的排列组合,可以先选1,然后选2or3,选了2然后只可以选3,依此类推。

leetcode47. 全排列 II 回溯剪枝的细节问题_第2张图片


        实际上,回溯的dfs就是在纵向地深度遍历,for循环就是在横向地遍历。

剪枝:

        首先,每次选了1以后,下一次肯定不能选1;如果已经选了1,2,那么下一次就不能选1和2,因此我们需要构建一个used数组来记录每个数字是否被选择,在每次循环开始前判断used是否被使用,从而实现剪枝。

        但这样只能应付上图的无重复情况,对于下图这种情况,我们发现不仅每次深度搜索需要去重,横向的循环也需要:例如,选第一个1、第二个1 、2 第二个1 、第一个1、2  都是[1,1,2],是重复的组合,这也需要排除。

leetcode47. 全排列 II 回溯剪枝的细节问题_第3张图片

         因此,我一开始就想,在每次循环内的开始,再加一个判断,只要当前元素和上一个元素不同(前提是有序数组),不就ok了?即,当循环到第二个1的时候,判断上一个是不是1,如果是,则return即可!见下面代码:

代码如下:

class Solution {
    List> res = new ArrayList<>();
    public List> permuteUnique(int[] nums) {
        Deque path = new ArrayDeque<>();
        Arrays.sort(nums);
        int len = nums.length;
        boolean[] used = new boolean[len];//used false
        backTrace(nums,path,used,len);
        return res;
    }
    public void backTrace(int[] nums, Deque path, boolean[] used,int len){
        if(path.size() == len){
            res.add(new ArrayList<>(path));
            return;
        }
        
        for(int i = 0; i < len; i++){
            if(used[i]){//保证深度搜索不会放置自己(不重复位置)
                continue;
            }

            //这一步,出现错误:
            if(i > 0 && nums[i] == nums[i-1])continue;//保证水平不重复元素
               
            used[i] = true;
            path.add(nums[i]);
            backTrace(nums,path,used,len);
            used[i] = false;
            path.removeLast();
        }
    }
}

        哈哈,测试发现输出结果是个[ ],空集合。明明纵向的去重了,横向也去重了,是哪里出错了呢?一步一步思考发现了问题如下。

leetcode47. 全排列 II 回溯剪枝的细节问题_第4张图片

 2.修正错误:

//修正
if(i > 0 && nums[i] == nums[i-1])continue;

        对于这里的水平去重,的确,每一轮循环到i的时候都可以判断是否和i-1元素相同,来实现不会选择值一样的元素,能够完成图中橙色的叉叉的剪枝。
        但是它在每一轮循环里,同时会剪掉纵向遍历过程的相同数字! 例如下图的绿色叉叉:

leetcode47. 全排列 II 回溯剪枝的细节问题_第5张图片


        因为,在绿色叉叉这一层,[1,1,2]这样的组合,不能够说 第二个1第 一个1相同就continue,那样的话这种正确答案就被排除了!
        关键:也就是说,第一个1仍在被使用(true)的时候,是可以加入别的相同的1的。与之相反的,当第一个1被置为false后,这样的等值判断才有效。
        
因此可以这样修改代码,见注释:

class Solution {
    List> res = new ArrayList<>();
    public List> permuteUnique(int[] nums) {
        Deque path = new ArrayDeque<>();
        Arrays.sort(nums);
        int len = nums.length;
        boolean[] used = new boolean[len];//used false
        backTrace(nums,path,used,len);
        return res;
    }
    public void backTrace(int[] nums, Deque path, boolean[] used,int len){
        if(path.size() == len){
            res.add(new ArrayList<>(path));
            return;
        }
        
        for(int i = 0; i < len; i++){
            if(used[i]){//保证深度搜索不会放置自己(不重复位置)
                continue;
            }
//只有在和上一个元素相等,并且上一个元素此时并没有被使用,
//说明已经执行了used[i] = false;说明已经用过,这才return。
//否则就是1,1,2的情况,第二个1是可以加入结果的。
            if(i > 0 && nums[i] == nums[i-1] && !used[i-1])continue;//保证水平,不重复元素
            used[i] = true;
            path.add(nums[i]);
            backTrace(nums,path,used,len);
            used[i] = false;
            path.removeLast();
        }
    }
}

        ok,答案正确。
        这个题目涵盖了纵向和横向的两种剪枝,并且需要注意横向的剪枝判断对纵向剪枝的影响,需要仔细思考。

你可能感兴趣的:(剪枝,算法,机器学习)