递归算法(5)——深度优先遍历(4)决策树

欢迎来到博主的专栏:算法解析

博主ID:代码小豪

文章目录

leetcode46------全排列

题目解析

这个问题有点像我们高中学排列组合的问题一样,将n个数的所有可能的组合给枚举出来。这个题意很好理解,因此博主不多赘述。

算法原理

由于题目的硬性要求枚举出所有可能的组合,和以往的算法题不同,通常我们都会将枚举出所有结果的算法称为暴力解法,如果在做OJ题的时候想到的算法只有枚举的话,那么这个题基本也就凉了一半了,但是既然题目要求我们枚举出所有的结果,那就试试吧。

如果你对这个题尝试想一个枚举的算法,你会发现连一个枚举的算法都很难想到(比如之前的一些问题,枚举算法的思路都是借助多个for循环,但是此题无法使用)。

不知道大家还记不记得高中时期学排列组合的问题的时候,书中有个使用决策数来帮助枚举的方法。那么我们来看看决策树是什么样的。

对于每个决策树来说,其根节点是一个起始状态start。每一个不同的决策都会产生一个分支,分治对应的子节点是关于该决策产生的结果,对于决策树,我们要保证枚举出的所有最终结果,都处于决策树上。

以题中的示例1为例,其nums={1,2,3},因此枚举出的所有的全排列结果都是有3个元素,我们假设每一个决策,都是将1,2,3放在枚举结果第i位上。过程如下:

在枚举所有决策时,有可能会出现错误决策,比如11()明显不符合题目要求,因为题目中说过数组nums不存在重复元素,而且也不符合组合的规则。因此在决策的过程中,我们需要将错误的节点进行剪枝。剪枝后的决策树如下:

最终的决策树如下:

通过观察发现,枚举的全排列结果都保存在决策树的叶节点,因此我们只需要遍历出所有的叶节点,也就得到了枚举的结果,如何快速找到树中的叶节点呢?那么也就是我们要以最快的速度,遍历到树的最终结果,那么很明显是使用深度优先遍历(以下简称dfs)。

在之前的章节当中,博主提到过递归算法天然就是一个dfs。因此我们设计一个递归函数来进行dfs。通过上面的讨论,我们可以确定以下细节:

(1)dfs的过程中需要剪枝

(2)我们需要记录下当前路径(以便来到叶节点时将路径的结果返回)

(3)由于回溯时,当前的状态会发生改变,因此我们需要恢复现场。

(4)找到递归出口

我们先来解决细节1。对于决策树是否需要剪枝,取决于决策当中元素是否已经位于全排列当中,因此我们可以创建一个bool类型的哈希表,来记录全排列元素在原数组的下标,比如1(下标0)已经在全排列当中,那么就要将对应的哈希表的值改为true。不存在全排列当中的元素则为false。如果当前决策的元素在哈希表中记录为true,则说明已经在全排列当中了,因此将该节点剪枝。

细节2的处理方式很简单,我们可以创建一个vector<int>容器来记录当前的路径(当前的全排列数组)。

由于发生回溯时,当前路径,以及位于全排列当中元素会发生变化,因此我们需要对记录全排列元素的哈希表以及记录路径的vector容器进行恢复现场,恢复现场的方式可以通过临时传参来完成(见上一篇博客)。但是由于我们使用vector容器来记录路径,哈希表记录全排列元素状态,因此如果是临时传参的话,每进入一个递归函数,都要实例化出一个vector容器和一个哈希表,因此对时间\空间的影响都是比较大的(内置类型比如int,double则不会,因此如果我们需要对一个STL容器进行恢复现场的话,最好不使用临时传参的方式)。

那么我们该如何做呢?首先我们任取决策树中的任意一个节点。

因此,我们可以创建一个全局变量path,哈希表hash,如果发生回溯,则将path的最后一个元素移除,哈希表对应的元素置为false。

最后则是递归出口,如果来到决策树的叶子结点,则将path记录在最终结果当中,然后retrun。

由于决策树只是一个抽象的模型,因此我们不需要真的用代码实例化出一个决策树出来。

题解代码

cpp 复制代码
class Solution {
private:
    void _dfs(const vector<int>& nums){
        if(path.size()==nums.size()){//path的元素个数会和nums的个数一致,说明来到叶节点
            ret.push_back(path);
            return;
        }
        for(int i=0;i<nums.size();i++){//开始进行决策
            if(hash[i]==false)//如果hash[i]==true,就不进行决策,相当于剪枝
            {
                path.push_back(nums[i]);//记录当前路径
                hash[i]=true;//记录当前元素位于全排列(即path)
                _dfs(nums);//结束_dfs说明已经回溯了
                path.pop_back();//恢复现场
                hash[i]=false;//恢复现场
            }
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        _dfs(nums);//开始深度优先遍历
        return ret;
    }
private:
    vector<int> path;//路径
    bool hash[7];   //哈希表
    vector<vector<int>> ret;//最终结果
};

由于需要枚举出所有的结果,如果有n个数要进行全排列,则有n!(阶乘)个枚举结果,因此时间复杂度为O(N!)。所以说枚举真的不是好办法--+

leetcode78------子集

题目解析

子集的概念是指,若集合A中的任一元素,在B中都能找到,则称A是B的子集。因此题目要求是让我们枚举出集合nums的所有的子集。由于不同顺序,但是元素相等的集合都是同一个集合,因此在此题中,元素相同但是顺序不同的子集不能作为另一个答案,比如{2,3}和{3,2}只能选择一个作为nums的子集

算法讲解

关于此类枚举算法问题,最好的方法是思考出一个合理的决策树,以示例1为例,对于nums={1,2,3},其子集都有以下特性:(1)子集包含nums第一个元素(即1)。(2)子集不包含nums的第一个元素。

由此我们可以筛选出两种不同的子集集合。然后继续划分,子集不包含nums的第二个元素,子集包含nums的第二个元素。可以又由此筛选出两种不同的子集集合。以此类推,直到nums的所有元素都被决策了。这么说好像不太好理解,我们可以画一下这个决策树。

由于在开始决策之前,没有任何一个元素存在于子集当中,因此决策树的根节点将会是一个空集。在进行决策之后,对应的节点是执行完该决策后的子集。

为了让图像清晰好看,后面(×N)表示子集中不包含N,(√N)表示子集中包含N,那么决策树如下:

通过观察可以发现,所有的枚举出来的子集,都在决策树的叶节点当中。因此我们要遍历整个决策树叶节点,并将叶节点作为节点返回。那么很容易想到要使用dfs算法。

那么接下来就是实现dfs算法的一些细节:

(1)剪枝:不需要剪枝

(2)使用全局变量path记录路径

(3)恢复现场

(4)递归出口

在画决策树的过程中,我们发现该决策树没有错误分支,因此不需要剪枝操作,由于回溯的过程中,路径会发生改变,因此需要对path进行恢复现场的操作。我们取决策树中的任意一个节点。

因此恢复现场的操作是:当回溯发生时,我们让path的最后一个元素删除。

由于我们需要遍历整棵树,因此来到叶节点之后,则是一个递归出口。

题解代码

cpp 复制代码
class Solution {
private:
    void _dfs(const vector<int>& nums,int pos){
        if(pos==nums.size()){//递归出口
            ret.push_back(path);
            return;
        }
        //子集中不存在nums[pos]
        _dfs(nums,pos+1);
        //子集中存在nums[pos]
        path.push_back(nums[pos]);
        _dfs(nums,pos+1);
        path.pop_back();
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        _dfs(nums,0);
        return ret;
    }
private:
    vector<vector<int>> ret;
    vector<int> path;
};

不同的决策树会产生不同的算法

我们还是以上一题为例,只不过这次决策树有些不同。我们继续观察示例1的子集,可以发现以下规律:

(1)子集第一个元素是1

(2)子集第一个元素是2

(3)子集第一个元素是3

然后继续划分

(1)子集第二个元素是2

(2)子集第二个元素是3

由于不存在子集第二个元素是2,这是因为子集的元素的顺序,不会构成新的子集,因此有了(1,2)这个子集之后,(2,1)这个子集与(1,2)这个子集是相同的,若将(2,1)枚举出来作为结果返回,则是一个错误答案。

以此类推。得到的决策树如下

观察决策树可以发现,每一个节点就是一个枚举的子集结果,因此我们需要遍历整颗树,可以使用dfs,也可以使用广度优先遍历,这里我们就是用dfs。

细节问题

(1)不需要剪枝

(2)需要用一个全局变量path记录当前路径

(3)回溯时需要恢复现场,恢复现场的方法和上一个例子一致

(4)每一个节点代表一个枚举结果,因此每进入一个新的路径,就要将当前路径作为答案返回

(4)递归出口:来到叶节点就是递归的出口

题解代码

cpp 复制代码
class Solution {
private:
    void _dfs(const vector<int>& nums,int pos){
        ret.push_back(path);
        if(pos==nums.size()) return;
        for(int i=pos;i<nums.size();i++){
            path.push_back(nums[i]);
            _dfs(nums,i+1);
            path.pop_back();//恢复现场
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        _dfs(nums,0);
        return ret;
    }
private:
    vector<vector<int>> ret;
    vector<int> path;
};
相关推荐
ClaNNEd@3 小时前
大话数据结构第二章,算法笔记
数据结构·笔记·算法
圣保罗的大教堂5 小时前
《算法笔记》9.3小节——数据结构专题(2)->树的遍历 问题 A: 树查找
算法
eamon1005 小时前
麒麟V10 arm cpu aarch64 下编译 RocketMQ-Client-CPP 2.2.0
c++·rocketmq·java-rocketmq
laimaxgg6 小时前
Qt窗口控件之颜色对话框QColorDialog
开发语言·前端·c++·qt·命令模式·qt6.3
乌云暮年7 小时前
算法刷题整理合集(四)
java·开发语言·算法·dfs·bfs
梁山1号7 小时前
【QT】】qcustomplot的初步使用二
c++·单片机·qt
bryant_meng7 小时前
【C++】Virtual function and Polymorphism
c++·多态·抽象类·虚函数·纯虚函数
Luo_LA7 小时前
【排序算法对比】快速排序、归并排序、堆排序
java·算法·排序算法
AI技术控7 小时前
机器学习算法实战——天气数据分析(主页有源码)
算法·机器学习·数据分析
oioihoii8 小时前
C++20 中的同步输出流:`std::basic_osyncstream` 深入解析与应用实践
c++·算法·c++20