【递归、搜索与回溯算法】专题三——穷举vs暴搜vs深搜vs回溯vs剪枝

文章目录

一、全排列

Leetcode链接

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = 1,2,3

输出:\[1,2,3,1,3,2,2,1,3,2,3,1,3,1,2,3,2,1]

解题思路

  • n=nums.length。这题最先想到的就是搞n层循环,每个循环固定一个数字,但如果n==100呢?难道要手写100层循环吗?
  • 本题要介绍全排列的一种很好的解法:使用递归代替循环。也就是字面意思,用递归代替每一层循环,每层递归中进行枚举出某个数位上的数字,当在一层递归中每固定一个数字就进行下一数位的枚举(也就是进入下一层递归),直到进行一次完整的全排列,此时回溯,回到上一层递归(也就是少上一数位)将这个数位的数换一个(这个时候肯定要进行"恢复现场"),继续重复往下一位枚举。
  • 全排列问题的枚举推导过程是可以画成上面这种"决策树"的,所以就很适合使用递归算法了解题,由"决策树"--->递归算法,这种思路要熟记,下一题也是这样的。

代码实现及解析

java 复制代码
class Solution {
    List<List<Integer>> ret;
    List<Integer> path;//记录每次更新的排列数(相当于二叉树中的每个路径)
    boolean[] used;//记录已经枚举过的数字
    public List<List<Integer>> permute(int[] nums) {
        ret=new ArrayList<>();
        path=new ArrayList<>();
        int n=nums.length;
        used=new boolean[n];
        dfs(nums);
        return ret;
    }
    void dfs(int[] nums){
        //递归出口:
        if(path.size()==nums.length){//path的每一位都确定了枚举的数字,可以return了
            ret.add(new ArrayList<>(path));//注意这里add的一定是path的副本
            return;
        }
        
        //path的位数还不够,继续枚举下一位
        for(int i=0;i<nums.length;i++){
            if(!used[i]){
                path.add(nums[i]);//添加上这一位数字
                used[i]=true;
                dfs(nums);//上面add已经固定了一位数字,继续尝试枚举下一位数字

                path.remove(path.size()-1);//这里是dfs回溯之后会执行到这里,要删除一位数,换一个数字add上(要进入下一层for循环,换个数进行固定)
                used[i]=false;//不要忘了删除这一位数后要将其的状态更改
            }
        }
    }
}

总结

  • 复习解题思路和代码实现及解析
  • 可以看到在本题中,ret.add()操作添加的是path的副本,千万不可以将path直接添加进去,不然ret中储存的就都是同一个引用,path在后续还会不断进行更改,会造成严重的错误,这是多路径问题非常常见的一个易错点,不要不在乎。而且就算是没有像本题一样使用全局变量,而是进行了传参,但如果参数是引用类型,依然可能会存在上述所指出的问题,所以在多路径问题、使用全局变量这样的场景中必须重视这个问题

二、子集

Leetcode链接

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = 1,2,3

输出:\[,1,2,1,2,3,1,3,2,3,1,2,3]

解题思路

方法一:

  • 对于【子集问题】我们可以对每个元素进行"留或不留"的决策,这是对于找子集问题的特点(包含不包含?不考虑排列)而推导出的方法。

遍历每个元素,对其进行选与不选的决定,画出决策图("×1"代表不选1,"√1"代表选1):

  • 可以看到每次对数组所有元素都进行了决策之后就会得到一个结果

方法二:

  • 第二种方法就是对于子集问题的结果我们可以发现其结果可以分类:不含有元素的(空集)、只包含1个元素的、包含两个元素的...
  • 所以我们可以分组进行处理,0元素的、有一个元素的... ,但是代码实现不是想当然地写的,我们可以在dfs中用for循环对每个元素进行遍历,这样来做出不同的选择,但每固定一个数(pos位置)仍需要直接进入下一位数(pos+1)的dfs,而且dfs中的for循环是从形参pos开始的,不然会导致子集重复。这样我们发现每次进入dfs其实都是一个结果,所以方法二的效率是比方法一高的。

决策图:

代码实现及解析

方法一:

java 复制代码
//解法一:
class Solution1 {
    List<List<Integer>> ret;
    List<Integer> path;
    public List<List<Integer>> subsets(int[] nums) {
        ret=new ArrayList<>();
        path=new ArrayList<>();

        dfs(nums,0);//从0下标开始遍历,对每个位置进行选择
        return ret;
    }
    public void dfs(int[] nums,int pos){
        //递归出口:遍历到了最后,也就是对每个位置都进行了"留与不留"的选择,得到了一个完整子集
        if(pos==nums.length){
            ret.add(new ArrayList<>(path));//再次强调要拷贝path的副本
            return;
        }

        //不选nums[pos]:
        dfs(nums,pos+1);

        //选nums[pos]:
        path.add(nums[pos]);
        dfs(nums,pos+1);
        //从上面这一句dfs出来后就要恢复现场:
        path.remove(path.size()-1);
    }
}

方法二:

java 复制代码
//解法二:
class Solution {
    List<List<Integer>> ret;
    List<Integer> path;
    public List<List<Integer>> subsets(int[] nums) {
        ret=new ArrayList<>();
        path=new ArrayList<>();
        dfs(nums,0);
        return ret;
    }
    void dfs(int[] nums,int pos){
        ret.add(new ArrayList<>(path));//每次dfs都是一个结果
        if(pos==nums.length) return;//递归出口

        /*这层dfs表示要对该数位上的数字进行选择,我们有多种选择,所以用for循环,
          但是i一定要从pos位置开始,不然就会导致子集重复(pos代表前面遍历到的位置)
        */
        for(int i=pos;i<nums.length;i++){
            path.add(nums[i]);//加上这一位数

            dfs(nums,i+1);//直接选择下一位数
            path.remove(path.size()-1);//从上面这个dfs出来,就要删除一位数,进入下一个for循环重新选择
        }
    }
}

总结

  • 复习解题思路和代码注释来回忆代码实现框架
  • 决策树的概念、能推导出决策树的题目都可以这样使用递归算法来解题
相关推荐
淡海水6 小时前
07-04-并发-ConcurrentBag-T-工作窃取WorkStealing算法
开发语言·算法·c#·bag·concurrent·workstealing
leobertlan6 小时前
好玩系列:训练一个神经网络模型指导小孩玩游戏2-大局观教练
算法
Tisfy7 小时前
LeetCode 3876.构造奇偶一致的数组 II:三种情况分类讨论(其实还是脑筋急转弯)
算法·leetcode·题解·脑筋急转弯
乐迪信息7 小时前
智慧港口船舶AI算法实现在线状态监测
大数据·人工智能·深度学习·算法·计算机视觉
来让爷抱一个8 小时前
2026 智能体安全实战:把护栏写进SPEC,MonkeyCode 云端跑通
人工智能·安全·机器学习
木井巳9 小时前
【BFS/DFS 解决 FloodFill 算法】太平洋大西洋水流问题
java·算法·leetcode·深度优先·广度优先·宽度优先·推荐算法
心抵鹊10 小时前
归并排序之翻转对(hard)
数据结构·算法
白山编程大哥10 小时前
Java 集合算法:从排序、查找到底层原理的实战指南
java·python·算法
shehuiyuelaiyuehao10 小时前
算法34,位运算符操作,总结
算法
Navigator_Z10 小时前
LeetCode //C - 1224. Maximum Equal Frequency
c语言·算法·leetcode