回溯初学必看

回溯初学必看

学完本文,你可以解决如下问题。

104. 二叉树的最大深度 - 力扣(LeetCode)

669. 修剪二叉搜索树 - 力扣(LeetCode)

77. 组合 - 力扣(LeetCode)

39. 组合总和 - 力扣(LeetCode)

46. 全排列 - 力扣(LeetCode)

回溯的目的??

我先问下读者,你说计算机最牛逼地方和人比是啥啊? 很多人都会说计算速度,我也承认这一点。问题来了,你让计算机去算一个曲面积分,和一个数学系的学生比一定快吗? 答案肯定是否定的,为啥??? 因为你计算曲面积分需要很多的准备工作,你得高速计算机啥是曲面积分?由此又衍生出了一系列前置问题,具体我就不说了。但是问题来了,他这个快是快在哪儿啊??快在简单的数理逻辑运算,也就是加减乘除模,移位,你让计算机算12321321*21321321412肯定比人快吧。我们回溯就是为了发挥计算机的这个特长,去解决一些没有很明显的优良算法的问题,再说大白话点,就是暴力枚举。但是你别忘了,以mac m2为例,每秒可以进行好像400e次简单的计算,其实是很大的一个数字。

初学者学回溯的时候往往都是,连这个概念都很懵逼,完全不知道该怎么下手。我以题目带知识点来讲解。

首先我默认大家已经很清楚的了解递归是个什么玩意儿了,我直接开始说题。

简单的递归

104. 二叉树的最大深度 - 力扣(LeetCode)

这个题很简单,我们想算最大深度,是不是就是max(左树的深度, 右树的深度) + 1就行了啊,为啥+1, 你不还有个根吗是吧。然后往下走,左树的深度不就是max(左树的左树深度, 左树的右树的深度) + 1啊,同理右树你也可以递归下去。对了这里我想说一件事,无论大家做什么题啊,回溯也好,dp,或者其他的问题,第一步一定要先控制好边界啊,也就是考虑极端情况,对于树来说常见的就是空树咋办,或者单叉链这种很极端的情况,大家要先考虑清楚再敲键盘,起码能过几个用例。

class Solution {
public:
    int maxDepth(TreeNode* root) {
        int depth = 0;
        if (root == nullptr)
            return 0;
        int leftmax = maxDepth(root->left);
        int rightmax = maxDepth(root->right);

        return 1 + max(leftmax, rightmax);
    }
};

我们观察一下这段代码,有几个很重要的block需要注意一下。我们咋确定递归不是无限深度啊,别给咱溢出了直接崩了? 也就是啥时候咱结束递归啊,结束条件是我们在递归的时候永远需要考虑的最重要的问题。对于上面这个题我们很容易就想到了,ok到了叶子就结束当前层的递归。

669. 修剪二叉搜索树 - 力扣(LeetCode)

class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if (root == nullptr)
            return NULL;
       if (root->val < low) {
            return trimBST(root->right, low, high);
        } else if (root->val > high) {
            return trimBST(root->left, low, high);
        }
        else
        {
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);

        return root;
        }
       

    }

这题和上面的一个原理,唯一区别就是我们需要比一下value是不是啊?我们让根的左边等于左边修剪好的子树,右边等于右边修剪好的子树,是不是就可以了。

加大难度

掌握了简单的递归,我们就可以来玩一下回溯了。回溯是咋回事,大白话就是我们试了一下,觉得不行,所以回到上一个状态,试试别的,再不行,再回,再试。

77. 组合 - 力扣(LeetCode)

1到n的所有组合啥意思? 大家高中都学过排列和组合,排列是有顺序的,组合是无顺序的。排列里2,1和1,2是俩结果,组合里他俩是一回事。比如这道题, n3的话, k2, 组合有几种? (1, 2) (1, 3) (2, 3)对吧。但是排列呢, (1,2)(1,3)(2,1)(2,3)(3,1)(3,2),有6种啊,读者要注意区分。

我们来看这道题,我当时第一反应做这种题,直接无脑for,你k不是等于2吗,我就套俩for,但是后来一想不对劲啊,k是可变的,万一k==5咋办呢? 5个for真的我就崩溃了。这时候咱的回溯就派上用场了。不管你K是几,我直接退回上一步,再重新选数字,利用递归来控制, 很多读者看到这还是一脸懵逼的啊。 我再细细分析下,首先我们需要收集组合的结果是吧,你比如1,2的时候我们为啥要输出啊,因为他正好俩数,等于我们的K了是不是啊,所以我们收集条件就是保存结果的数组等于k了,就保存起来。然后呢?我们现在有1,2了,下一步是不是该1.3了啊。咋收集1,3啊, 我们先把2 pop出去,然后把3拿进来,也等于K,又是一个新的结果。 逻辑搞清楚了,我们咋实现代码啊? 我们这里用一个开始索引,啥意思呢, 你现在是1到n, 起始数字就是1, 我们1放进去了,下一个你还能放1吗,是不是只能从2开始啊, 所以你开始的位置是固定死的,但是你下一个要往后遍历。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;

    void backtrack(int n, int k, int startindex)
    {
        if (path.size() == k)
        {
            res.push_back(path);
            return;
        }

        for (int i = startindex; i <= n ; i++)
        {
            path.push_back(i);
            backtrack(n, k , i + 1); //这里为啥是k+1啊, 就是我说的,你用了1还能用1吗, 是不是该2了啊
            path.pop_back();//1,2结束了你是不是得把2拿下来啊,再进行下一轮循环
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtrack(n, k , 1);

        return res;
    }
};

加大亿点点

39. 组合总和 - 力扣(LeetCode)

做完上一题,我们看下这一个题目是咋回事啊, 这个题目难点在于,上一个都是选一次,这个一个可以重复选,读者又懵了,这重复我咋知道用几次啊??? 其实我们可以考虑一种极端情况, 你比如target目标和是5, 我们元素就一个1,让你算组合,我们不就是抓着1使劲薅羊毛吗?? 我们直接1给他用5次就得到结果了。 那复杂点, 比如样例给的这个,那我们最深的深度是不就是2用4次啊,发现不行,我们回退到2用三次,和别的数字开始凑是不是就行了啊。逻辑上就是这样的。 那么结束条件是啥啊,很简单我们定义个sum保存和,啥时候sum==target就保存到结果集里面是不是啊。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WrIVRHUu-1670745693544)(C:\Users\41988\AppData\Roaming\Typora\typora-user-images\image-20221211154713245.png)]

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;

    void backtrack(vector<int>& candidates, int target, int sum, int startindex)
    {
        if (sum > target)
            return;
        if (sum == target)
        {
            res.push_back(path);
            return;
        }

        for (int i = startindex ; i < candidates.size(); i++)
        {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtrack(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtrack(candidates, target, 0, 0);
        return res;
    }
};

有的读者看到sum-=懵了, 这是咋回事啊,其实和我刚才说的一模一样。你用了4个2发现凑不出来啊,但是这时候你的sum是不是已经是8了啊,你都超了,所以得-2, 再去和别的数字凑。 剩下的逻辑还是一样的,遍历candidate就可以了。

简单的排列

46. 全排列 - 力扣(LeetCode)

正如我前文说的,排列唯一的区别就是排列是有顺序的,你1开头的时候,可以用2, 2开头的时候可以用1, 我们凡是遇到可以重复用数字的这种题目啊,只要开一个bool数组,记录下你用没用过就可以了。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;

    void backtrack(vector<int>& nums, vector<bool>& used)
    {
        if (path.size() == nums.size())
        {
            res.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); i++)
        {
            if (used[i] == true)
            {
                continue;
            }
            path.push_back(nums[i]);
            used[i] = true;
            backtrack(nums, used);
            used[i] = false;
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        backtrack(nums, used);
        return res;
    }
};

第一步还是看看结束条件,啥时候停止啊,全排列,就是我们的答案必须和原数组一样是不是结束递归啊。回溯条件呢? 是不是回退到了上一步,记得把这个bool数组的对应值改成没用过,然后从末尾拿下来就行了啊。你1,2,3拿到结果了,开始回退,你开始的时候要控制bool数组,要是用过了就跳过本次循环了。

总结

回溯是笔试爱考,检验代码功力,考察边界控制的一个很综合,难度也大的题。很多读者都很懵逼啊一开始接触到这种题。但是我们只要把把握住主线,两个:1结束条件,不然无限月读溢出了是吧 2.回溯的时候有很多添加的细节是不是啊,比如bool数组啊, sum加减啥的, 最主要的是,理解这个逻辑思路很重要啊,其实大家看多了就知道回溯一般都是多叉树啊,也就是更复杂的多路递归,我们只要把握住回溯函数的写法,套路基本都差不多,感谢观看。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值