全排列Ⅱ(中等难度,加入剪枝操作)

这篇博客详细解析了LeetCode中全排列II的解题思路,主要涉及回溯算法与深度优先搜索的结合,并重点介绍了如何在存在重复元素的情况下进行有效的剪枝操作。博主分享了weiwei大佬的题解链接,解释了为何需要对数组排序、如何判断并避免重复全排列,以及剪枝条件的设定。文章还探讨了在处理重复元素时判断路径与层级的重要性,确保正确剪枝的同时不丢失有效解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目概述(中等难度)

在这里插入图片描述

题目链接:
点我进入leetcode

思路与代码

思路展现

这道题目就是经典的回溯+DFS(深度优先搜索遍历),在这里我就不再给出我关于回溯的讲解了,leetcode的大佬已经帮我们总结到位了,只需要跟着他的题解走即可,在这里宣传以下weiwei大佬是一位非常厉害的大佬,除了他自己写的题解意外,也开设了自己的github网站帮助大家学习算法,在这里我都会将其网站贴到下面:
全排列Ⅱweiwei大佬题解链接
weiwei大佬github链接

这道题目是全排列题目的再进一步延申,只不过这道题目加入了两个条件,第一个就是这个序列中的元素有重复的,第二个就是需要按照任意顺序返回不重复的,在之前的全排列题目中我们的序列没有重复元素,所以最终我们返回的全排列就是没有重复元素的,而这次我们序列是有重复元素的,那么最终我们返回的全排列就肯定有重复元素,所以此时就需要用到剪枝,什么是剪枝以及如何用大家直接看weiwei大佬的题解就行啦,链接我已经放到了上面.

其实这段代码针对于之前的全排列其实就只多了几行代码,如下所示:
第一处:
首先要对我们的数组进行排序,因为数组有序是剪枝的前提

Arrays.sort(nums);

第二处:

if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
    continue;
}

我们来解释下为什么要加上面的代码
1:首先一个比较容易想到的办法是在结果集中去重。但是问题来了,这些结果集的元素是一个又一个列表,对列表去重不像用哈希表对基本元素去重那样容易。

如果要比较两个列表是否一样,一个容易想到的办法是对列表分别排序,然后逐个比对。既然要排序,我们就可以 在搜索之前就对候选数组排序,一旦发现某个分支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复列表,所以此时我们用到了剪枝.
2
为什么要加上nums[i] == nums[i - 1]?
例如对于【1,1’,2】这个数组,如果我们加上这个条件的话,相当于1’这个会产生与之前1相同的全排列结果的数字就不会再进行判断,直接跳到2去
但此时同学们思考一个问题,光加上这个条件够吗?
答:当然不够,原因如下
假设只判断这两个是否相等,很容易将我们本来符合条件的全排列删掉,例如假设如果是【1,1,2】这种组合,最终输出的结果是不存在全排列,因为前后只要重复就全部被剪掉了,所以说我们最终需要加上额外的判断条件
在这里插入图片描述
放到我们OJ的测试用例里面我们会发现有如下测试用例不通过:
在这里插入图片描述

此时为什么会有上面的问题出现,这是因为我们在判断具有重复元素的时候,并没有判断这两个重复的这个元素到底是在同一路径还是同一层,如果是同一路径是不能被剪枝的,而在同一层是可以被剪枝的,而这时我们判断两个重复的元素是在同一条路径还是在同层路径的标准是nums[i - 1] == false来进行判断的,假如此时nums[i - 1] = false的话,就说明我们这两个重复元素是在同层:如下所示:此类情况是必须进行剪枝的:原因是我们之前的1已经被选择过,并且状态从true置为了false,而此时这个新的1跟刚才1情况相同,所以就不需要再走一遍了,直接进行剪枝就好
在这里插入图片描述
而当nums[i - 1] = true的话,此类情况如下:
在这里插入图片描述
这种情况就说明是不能剪枝的.
总结:
在这里插入图片描述

代码示例

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {




      //len代表数组的长度
       int len = nums.length;
       //path是一个双端队列,使用Deque的原因是官方题解所给出的
       Deque<Integer> path = new LinkedList<>();
       //设置我们的res
       List<List<Integer>> res = new ArrayList<>();
       //设置我们所给定的nums数组中每个元素的初始状态为false
       boolean[] used = new boolean[len];

       if(len == 0) {
           return res;
       }

       //排序是剪枝的前提
       Arrays.sort(nums);

       
       dfs(path,used,nums,res,0,len);
       return res;
    }

    
    /*
    dfs方法参数介绍:
    path表示双端队列,用于存储我们所选择的路径上的数字
    boolean数组用于表示数字的选择与被选择,被选择为true,没有为false
    res用于存储最后的返回结果
    我们递归结束的条件与我们nums数组的长度len以及二叉树深度depth有很大的关系
    */
    public void dfs(Deque<Integer> path , boolean[] used , int[] nums , List<List<Integer>> res , int depth , int len) {

       if(depth == len) {

           //注意这里的写法其实是一个简便写法,就是直接实例化一个list集合然后将双端队列参数直接传入进去
           res.add(new ArrayList<>(path));
           //声明我们的递归终止条件
           return;

       }

       //!used[i]就表示此时假设这个数字还没有被选中,它的反就一定为true,然后才能进入到下面的语句
      
           for(int i = 0 ; i < len ; i++) { 
               if(!used[i]) {

               // 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
               // 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
               if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                   continue;
               }

               //选中后将数字加入到path中
               path.addLast(nums[i]);
               //选中后将状态置为true
               used[i] = true;
               //上面我们选择元素完毕后就开始我们的递归
               dfs(path  ,used , nums , res , depth + 1 , len);
               //进行完递归操作后接下来就需要我们进行撤回操作
               //1:撤回操作的第一步是将状态置为false
               used[i] = false;
               //2:撤回操作的第二步是将数字从path中移除
               path.removeLast();

           }
       }
    }
}

在这里插入图片描述

<think>好的,用户问的是“凑算是是全排列还是剪枝类型的”,首先需要确认“凑算”具体指的是哪种算法。根据常见的算法分类,可能是指“穷举搜索”中的一种方法,比如回溯法中的全排列生成或者剪枝优化。 首先,全排列算法属于穷举所有可能的排列情况,例如生成n个元素的所有排列,时间复杂度是O(n!),这在元素较多时效率很低。而剪枝通常用于优化搜索过程,提前排除不可能的情况,减少计算量,常见于回溯法和分支限界法中。 接下来,分析“凑算”可能的含义。如果指的是通过尝试不同的排列组合来找到解,例如解决数独或八皇后问题,这时候通常会使用回溯法,既涉及生成排列(全排列的一种形式),又需要在发现当前路径不可能正确时进行剪枝,放弃后续的搜索。 因此,凑算可能结合了全排列的生成和剪枝的策略。比如在解决某些数学题或组合优化问题时,先生成可能的排列,再通过条件判断提前终止无效的分支。这种情况下,它不属于单纯的全排列剪枝,而是两者的结合应用。 还需要考虑用户可能的上下文。如果问题场景是需要遍历所有可能的排列但通过某些条件减少计算量,那么剪枝是其中的优化手段。例如,在旅行商问题中,生成所有路径排列但剪枝掉超过当前最优解的路径,从而减少计算时间。 总结来说,凑算如果指的是回溯法中的策略,那么它同时涉及全排列的生成和剪枝的优化,属于两者的结合。因此需要根据具体的应用场景来判断,但通常这类方法归类为剪枝类型的优化,因为它们的主要目的是通过剪枝提高效率,而全排列只是其中的一部分基础步骤。</think>凑算法通常属于**剪枝(Pruning)类型的优化策略**,而非单纯的全排列方法。以下是逐步分析: --- ### 1. **基本概念区分** - **全排列(Permutation)**: 指**穷举所有可能的排列组合**,例如生成3个元素的所有排列是:`[1,2,3]`, `[1,3,2]`, `[2,1,3]`等。时间复杂度为$O(n!)$,效率低但覆盖所有可能性。 - **剪枝(Pruning)**: 在搜索过程中**提前终止无效路径**,放弃不可能得到解的子树。例如在回溯算法中,当发现当前路径不满足条件时,立即停止探索后续分支。 --- ### 2. **凑算法的核心逻辑** - **目标**:通过“尝试性组合”快速找到可行解,而非遍历所有可能。 - **典型场景**: 解决组合优化问题(如数独、八皇后、子集和问题)时,**边生成候选解,边验证条件**。 - 若当前部分解已不满足约束(如数字重复、和超过目标值),则**剪去该分支**,不再生成后续排列。 - 若部分解仍有希望,则继续扩展。 --- ### 3. **为什么说凑算法是剪枝类型?** - **关键操作**: 在生成排列的过程中,**动态判断是否继续**,避免无效搜索。例如: ```python def backtrack(path): if 不满足条件: # 剪枝判断 return if 找到解: 输出结果 for 选择 in 所有可能选项: 做选择 backtrack(新路径) 撤销选择 ``` - **与全排列的区别**: 全排列会生成所有$n!$种可能,而凑算法通过剪枝可能只探索了部分排列。 --- ### 4. **实例说明:子集和问题** - **问题**:找到数组`[2,4,6,8]`中和为10的子集。 - **全排列法**:检查所有子集(共$2^4=16$种)。 - **剪枝法(凑算法)**: - 当路径和已超过10(如`[2,4,6]`和为12),立即停止后续添加。 - 实际只需检查少量子集即可找到解(如`[2,8]`, `[4,6]`)。 --- ### 5. **总结** - **凑算法本质**:基于剪枝的优化策略,**结合了部分排列生成与条件判断**。 - **适用场景**:解空间大但存在明确约束的问题(如组合优化、约束满足问题)。 - **优势**:通过减少无效搜索,显著提升效率,时间复杂度通常低于全排列
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值