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

文章讨论了一种使用回溯算法解决排列组合问题时处理重复元素的策略,包括纵向和横向的剪枝方法。作者通过分析错误代码,指出在横向遍历时,只有当元素未被使用时,才能剪枝避免重复。最后,作者提供了修正后的代码,实现了正确处理重复元素的排列组合问题。
摘要由CSDN通过智能技术生成

题目描述:

 1、思路

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


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

剪枝:

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

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

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

代码如下:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        Deque<Integer> 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<Integer> 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();
        }
    }
}

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

 2.修正错误:

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

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


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

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        Deque<Integer> 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<Integer> 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,答案正确。
        这个题目涵盖了纵向和横向的两种剪枝,并且需要注意横向的剪枝判断对纵向剪枝的影响,需要仔细思考。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

好奇的7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值