算法学习(十五)—— 回溯算法

目录

穷举vs暴搜vs深搜vs回溯vs剪枝

46.全排列

78. 子集

解法一

解法二


穷举vs暴搜vs深搜vs回溯vs剪枝

这些名词最开始学算法的时候看着可能一头雾水,但是本篇文章已经是第15篇了,这一路下来可以发现,上面这些名词其实很不是很神秘,深搜就是二叉树递归,回溯就是往回走,剪枝就是加几个判断而已

下面我们通过两非常典型的回溯的例题来全面搞懂这些东西

46.全排列

46. 全排列 - 力扣(LeetCode)

题目很简单,就是让我们返回全排列的结果,这道题题目并不含有重复数字,含有重复数字的是 “全排列 Ⅱ”,这个我们后面再讲,下面咱们来分析下这道题:

  • 这一道题本质是穷举类的题,我们前面已经见到过很多,最简单的解法就是用两层循环暴力枚举;而这道题,就是数组中有多少个数就用多少个循环,像示例一有3个数就用三个循环。但是问题来了,万一题目给的数组有100个数呢?那就要用100个循环,显然会超时
  • 首先是第一步,我们先画一个详细的决策树,以示例一为例,如下图:
  • 当画出决策树并找到相同子问题时,我们的决策树就可以翻译成代码
  • 就和上一篇博客一样,我们先来设计下“全局变量”: vector<vector<int>> vv,作用是保存最终的结果;vector<int> path,这个就是在我们深度优先遍历的时候,记录下路径,也就是保存每一次全排列的结果
  • 模拟的过程也很简单,以上面决策树的最终结果为123的那一条路径为例,当path的长度等于题目数组的长度时,完成一次全排列,然后回溯,干掉 “123” 的后面两个数,“恢复现场”为“1 __ __”然后往右走3的路径。这个步骤简单,但是我们要如何处理“剪枝”呢
  • 所以我们还需要一个全局变量:bool[] check,是一个bool类型的数组,作用是判断一个数在一个路径里已经是否被使用,当“1 __ __” 选择 2 时,就把check[1] 设置为 true,意思就是 1 位置的这个数 “2” 已经被使用过了,后面每次枚举时判断一下,在 check中找找有没有用过,就可以实现剪枝
  • 然后就是设计 dfs 函数,也很简单,只需关心某一个节点在干什么事情,就是把数组中所有的数枚举一遍,只要检查check有没有用过,用过就是返回,没用过旧添加进path后面

细节处理:

  • 回溯:当path往后返回时,把最后一个数干掉;并且还要把 check 里的该位置的true改为false
  • 剪枝:check实现
  • 递归出口:遇到叶子节点时,直接添加结果即可,然后返回
class Solution {
public:
    vector<vector<int>> vv;
    vector<int> path;
    bool check[7];

    vector<vector<int>> permute(vector<int>& nums) 
    {
        dfs(nums);
        return vv;
    }
    void dfs(vector<int>& nums)
    {
        if(nums.size() == path.size()) //遇到叶子节点就添加结果
        {
            vv.push_back(path);
            return;
        }

        for(int i = 0; i < nums.size(); i++)
        {
            if(check[i] == false)
            {
                path.push_back(nums[i]);
                check[i] = true;
                dfs(nums);
                //回溯(恢复现场):和上面的反着来即可
                path.pop_back();
                check[i] = false;
            }
        }
    }
};

78. 子集

78. 子集 - 力扣(LeetCode)

这道题考察的知识点就是我们高中时学的子集问题,但这个题目有点不一样,比如[1, 2, 3] 和 [2, 3, 1] 是同一个子集,因为所含数字相同,所以要注意区分,下面来分析下这道题:

解法一

  • 解法步骤和上面是一样的:①搞出一个决策树    ②设计全局变量和dfs函数主题    ③处理细节问题
  • 先来搞一个决策树,如下图:
  • 然后就是设计全局变量,vector<int> path,作用是记录每个路径选或者不选之后的一个结果,vector<vector<int>> vv,用来记录最终结果,而且通过上面的决策树可以发现,这道题并没有剪枝操作
  • 再然后就是dfs的函数体设计,dfs[nums, i],我们不仅仅要知道当前这个数组,还要告诉我接下来要选的是哪个位置的数,所以dfs要干的事就是判断 “ i ” 这个数选还是不选即可
  • 如果要选,就是 path += nums[i] 后dfs到下一层;如果不选,就啥也不干,直接dfs到下一层

细节处理:

  • 回溯:如果选择 i 加入,加入后dfs没问题;但是当dfs完回溯的时候,需要把path最后的值干掉,也就是 dfs -= nums[i]
  • 递归出口: 当 i == nums.size() 时,把path扔 vv 里去即可

 

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

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

        //选择这个数
        path.push_back(nums[i]);
        dfs(nums, i + 1);
        path.pop_back(); //恢复现场

        //不选,直接递归
        dfs(nums, i + 1);
    }
};

解法二

  • 步骤也是:①画决策树    ②设计全局变量和dfs函数体    ③细节问题
  • 解法二的决策树的选择策略,就是依据元素个数来选,如下图:
  • 全局变量和解法一的vv和path一样
  • 然后就是dfs的函数体,dfs[nums, pos],pos表示数组下标,含义就是“下一次添加数字时,从数组的哪里开始”,每枚举出一个结果,就把该结果扔 vv 里去,然后从i + 1的位置继续dfs

细节处理:

  • 刚开始进到函数体的时候,直接收集结果 

 

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

    void dfs(vector<int>& nums, int pos)
    {
        //解法二,每次刚开始,path都是一个结果
        vv.push_back(path);
        for(int i = pos; i < nums.size(); i++)
        {
            path.push_back(nums[i]);
            dfs(nums, i + 1);
            path.pop_back();
        }
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值