【算法】{画决策树 + dfs + 递归 + 回溯 + 剪枝} 解决排列、子集问题(C++)

1. 前言

  1. dfs问题 我们已经学过,对于排列、子集类的问题,一般可以想到暴力枚举,但此类问题用暴力解法 一般都会超时,时间开销过大。
  2. 对于该种问题,重点在于尽可能详细的 画决策树,随后根据决策树分析 题目所涉及的 剪枝、回溯、递归等细节问题。
  3. 根据决策树的画法不同,题目会有不同的解法,只要保证决策树没有问题,保证细节问题下 代码一定可以编写出来。

2. 算法例题 理解思路、代码

46.全排列

在这里插入图片描述

思路

  • 思路:求出数组中元素的所有排列顺序,并用数组输出。

  • 解法一 暴力枚举

    • 用n层for循环,每层循环依次固定一个数
    • 超时,时间开销太大,n个元素就是O(n ^ n)
  • 解法二 根据决策树 进行递归
    在这里插入图片描述

    • 根据上图,我们需要创建下面三个全局变量.
      在这里插入图片描述
    • 结束条件:当我们遍历到叶子节点时(即path.size() == nums.size()),将path加入到ret中,并向上返回。
    • 回溯:对当前元素dfs后,进行回溯,回溯即将之前加入到path 的元素删除,并将used重新置为false。

代码

class Solution {
public:
    vector<vector<int>> ret; // 用于存储最终结果
    bool used[7]; // 用于记录某个下标的元素是否在序列中
    vector<int> path; // 用于记录某个下标的元素是否已经加入到序列中
    vector<vector<int>> permute(vector<int>& nums) {
        dfs(nums);
        return ret;
    }

    void dfs(vector<int>& nums) {
        if(path.size() == nums.size())
        {
            ret.push_back(path);
            return;
        }

        // 遍历数组,对每一位都进行dfs && 排列
        for(int i = 0; i < nums.size(); ++i)
        {
            if(used[i] == false) // 如果该位置未加入到当前序列中
            {
                path.push_back(nums[i]);
                used[i] = true;
                dfs(nums);
                // dfs向上返回回来 -> 回溯
                path.pop_back();
                used[i] = false;
            }
        }
    }
};

78.子集

在这里插入图片描述

  • 题目要求我们将数组的所有子集统计,并以数组形式返回(空集就是空数组)

解法一

  • 解法 根据 选与不选 画决策树
    在这里插入图片描述

    • 根据上图决策树,我们通过对一个元素的选择与否划分树,而当到达叶子节点的时候(i == nums.size())向上返回即可。
    • 函数头:首先需要的参数是数组本身,其次我们通过变量i来标记当前选择的元素所在层数,则 void dfs(vector<int>& nums, int i)
    • 函数体:分别写出选择与不选择该元素时的代码即可
    • 结束条件:如前面所说,当 到叶子节点时返回。

代码

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    vector<vector<int>> subsets(vector<int>& nums) {
        int i = 0;
        dfs(nums, i);
        return ret;
    }

    void dfs(vector<int>& nums, int i)
    {
        if(i == nums.size())
        {
            ret.push_back(path);
            return;
        }

        // 不选当前元素
        dfs(nums, i + 1);

        // 选当前元素
        path.push_back(nums[i]);
        dfs(nums, i + 1);
        path.pop_back();
    }
};

解法二

在这里插入图片描述

  • 解法 根据子集包含的元素个数 画 决策树
  • 如上图所示,以此法画的决策树,每个节点的值都是有效值
    • 函数头:第一个参数是数组本身,另外需要给出当前遍历到nums的第几个元素。void dfs(vector<int>& nums, int pos)
    • 函数体
      1. 在函数开始时先将当前子集加入到ret中
      2. 利用for循环,每次从pos开始遍历数组:每次将一个元素作为子集第一位的所有子集检索完毕后,再以下一个元素作为子集第一位,可以防止重复子集
        • for循环中每次将当前元素加入到path中,dfs下一位,最后回溯

代码

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    vector<vector<int>> subsets(vector<int>& nums) {
        int pos = 0;
        dfs(nums, pos);
        return ret;
    }

    void dfs(vector<int>& nums, int pos)
    {
        ret.push_back(path);
        for(int i = pos; i < nums.size(); ++i)
        {
            path.push_back(nums[i]);
            dfs(nums, i + 1);
            path.pop_back(); // 回溯 - 恢复现场
        }
    }
};

3. 算法题练习

1863.找出所有子集的异或总和再求和

在这里插入图片描述

思路

  • 题目分析:题目要求求出数组中所有子集的异或和的总和,我们只需要根据上图求子集的思路,在统计子集时,直接用变量计算异或值即可
    • 解法 dfs + 决策树
      • 这道题的决策树与上题一致,只需要在执行方面进行更改。
      • 回溯:由于一个元素异或一个数两次,相当于没有异或,所以对于回溯操作,我们只需要再次进行异或即可。

代码

class Solution {
public:
    int ret = 0; // 最终结果
    int xorSum = 0; // 记录一个子集的异或和
    int subsetXORSum(vector<int>& nums) {
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos)
    {
        ret += xorSum;

        for(int i = pos; i < nums.size(); ++i)
        {
            xorSum ^= nums[i];
            dfs(nums, i + 1);
            xorSum ^= nums[i]; // 回溯现场 / 异或同一个数两次相当于无异或
        }
    }
};

47.全排列II

在这里插入图片描述

思路

  • 题目分析:
    在这里插入图片描述

  • 根据上图,制定决策树
    在这里插入图片描述

  • 下面是对于上面决策树的解释,以及根据该决策树,我们如何设计代码
    在这里插入图片描述

  • 我们对上面探讨的两种解法进行解释:
    在这里插入图片描述

    • 如图所示,如果用文字解释,对于不合法路径:
      • 当 【当前元素A已经使用了 && 该分支下已有与当前元素值相同的B && B已经在序列中】时不合法。
  • 编写代码方面,本道题与前面的题非常类似,主要在于主逻辑的差别:

代码

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    bool used[9];

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end()); // 先排序数组
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int> nums, int pos)
    {
        if(path.size() == nums.size())
        {
            ret.push_back(path);
            return;
        }

        for(int i = 0; i < nums.size(); ++i)
        {
            // 剪枝 - 考虑合法路径
            if(used[i] == false && (i == 0 || nums[i] != nums[i - 1] || used[i - 1] == true))
	        {
                path.push_back(nums[i]);
                used[i] = true;
                dfs(nums, pos + 1);
                path.pop_back();
                used[i] =  false;
            }
        }
    }
};

17.电话号码的字母组合

思路

在这里插入图片描述

  • 解法 dfs + 决策树
    • 决策树:如下图所示

在这里插入图片描述

  • 本体决策树画出来后,递归回溯等部分相对于前面简单一些
    • 细节问题
      • 我们需要由数字与字符串一一对应来进行号码的模拟,这里可以用哈希表 ——> 优化:数组作为哈希表hash
      • 主逻辑:关于for循环,我们需要从digits中依次提取数字字符,并找到相对应的字符串,即hash[digits[pos] - '0']

代码

class Solution {
public:
    // 数组作哈希,下标对应字符串
    string hash[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    string path;
    vector<string> ret;
    vector<string> letterCombinations(string digits) {
        // 特殊情况
        if(digits.size() == 0) return ret;

        dfs(digits, 0);
        return ret;
    }

    void dfs(string& digits, int pos) {
        if(path.size() == digits.size())
        {
            ret.push_back(path);
            return;
        }

        for(char ch : hash[digits[pos] - '0']) // 提取数字字符
        {
            path.push_back(ch);
            dfs(digits, pos + 1);
            path.pop_back();
        }
    }
};

有待更新… …

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《机器学习》西瓜书是机器学习领域的一本经典教材,其中的决策树算法是机器学习中的一种重要分类算法决策树算法可以用于决策问题,将问题分解成多个子问题,通过构造决策树归地进行分类。 决策树算法的构建过程可以分为两个步骤,即特征选择和决策树生成。在特征选择过程中,需要根据某个评估指标对不同特征进行排序,选择最优的特征作为节点进行分割。常用的评估指标包括信息增益、信息增益比和基尼系数等。在决策树生成过程中,需要归地生成决策树的各个节点,通过特征选择将训练样本不断划分成子集,并为每个子集生成一个新的节点,直到满足停止条件。 决策树算法具有易理解、易实现的特点,同时对部分异常数据具有一定的鲁棒性。但是,在处理高维数据或特征较多的数据集时,决策树算法可能会存在过拟合等问题。为了解决这些问题,可以使用剪枝算法、随机森林等方法进行优化和改进。 在实际应用中,决策树算法被广泛应用于数据挖掘、信用评估、医学诊断、文本分类等领域。在学习和应用决策树算法时,需要注意特征选择和决策树生成的各种细节和算法选择,以及如何利用决策树算法解决实际问题。 ### 回答2: 《机器学习》这本西瓜书是机器学习领域的经典教材之一,其中涉及了决策树算法决策树是一种基于树形结构的分类方法,可以用于处理离散型和连续型数据集。使用决策树算法建立模型的过程,可以理解为归地将数据切割成小的子集,使得每个子集的纯度尽可能地提高,最终生成一棵有序的树型结构。 决策树算法的训练过程,通常分为三个步骤:选择最优特征、建立决策树以及剪枝。其中选择最优特征的目的是在当前样本集合中,找到对样本分类最有帮助的特征,通过衡量每个特征的信息增益或信息增益比,选出最优特征作为节点。根据节点特征将数据集分成若干互斥的子集,然后归地对子集进行划分,生成决策树。最后,通过剪枝减少决策树的复杂度和泛化误差,得到最终的模型。 决策树算法在实际应用中具有很高的灵活性和可解释性,相对简单的分类问题中具有很好的性能。但是,当数据集过大或过于复杂时,决策树算法的计算复杂度会显著增加,生成的决策树容易过拟合,泛化能力较差。因此,在进行模型训练时需要进行特征选择、代码优化以及剪枝等操作。 ### 回答3: 决策树是机器学习中一种常用的算法,它采用树状结构来进行分类和预测。在《机器学习》西瓜书中,决策树被归为监督学习中的分类算法决策树算法的主要思想是将数据按照特征属性分为不同的类别。决策树有三个关键的概念:节点、分支、叶子节点。节点包括根节点、内部节点和叶子节点。根节点代表数据集,内部节点表示特征属性,叶子节点代表不同的数据类别。 在决策树算法中,有两种常用的构建方式:ID3算法和C4.5算法。这里我们简要介绍一下C4.5算法。C4.5算法决策树算法中的一种改进算法,它不仅考虑了信息熵,还考虑了各个特征属性之间的相关性,从而提高了决策树算法的准确率。 C4.5算法主要分为三个步骤:特征选择、决策树的生成和决策树剪枝。在特征选择阶段,C4.5算法采用信息增益比来选择最优划分属性。在决策树的生成阶段,C4.5算法采用归方法,依次生成决策树的各个节点。在决策树剪枝阶段,C4.5算法通过比较剪枝前后的错误率来确定是否进行剪枝。 总的来说,决策树算法是一种简单且常用的分类算法,它不仅易于理解和解释,还具有较高的分类准确率。当然,在实际应用中,我们需要根据实际情况选择合适的决策树算法,并对模型进行调参和优化,提高算法的性能和实用性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值