• 代码随想录Day20 回溯算法 LeetCode77 组合问题


    以下内容更详细解释来自于:代码随想录 (programmercarl.com)

    1.回溯算法理论基础

    回溯法也叫回溯搜索法,是搜索法的一种,我们之前在二叉树中也经常使用到回溯来解决问题,其实有递归就有回溯,有的时候回溯隐藏在递归之下,我们不容易发觉,今天我们来详细介绍一下什么是回溯,它能解决哪些问题.

    回溯法效率

    回溯法的效率是不高的,回溯的本质是穷举,因为有些问题能用回溯法解决出来就不错了,别无他法,只能使用这个暴力方法

    回溯法,一般可以解决如下几种问题:

    • 组合问题:N个数里面按一定规则找出k个数的集合
    • 切割问题:一个字符串按一定规则有几种切割方式
    • 子集问题:一个N个数的集合里有多少符合条件的子集
    • 排列问题:N个数按一定规则全排列,有几种排列方式
    • 棋盘问题:N皇后,解数独等等
    • 提供一个八皇后小游戏hhh:【死亡8皇后】小游戏_游戏规则玩法,高分攻略-2345小游戏

    理解回溯法的方式

    不要光靠脑子想,要将这种回溯具象化,想象成树形结构,任何回溯法解决的问题都可以转化为树形结构来解决问题.

    因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度.

    递归就要有终止条件,所以必然是一棵高度有限的树(N叉树).

    回溯法代码模板(伪代码)

    首先是函数参数和返回值

    一般无需返回值,参数很多,一般边写边定,函数名一般定为backtracking

    void backtracking(参数)

    终止条件

    我们说可以将回溯算法想像成一个树形结构,那么我们就一定有终止条件,一般到达终止条件(叶子结点),也就是我们收获答案的时候,相关为伪代码如下

    1. if (终止条件) {
    2. 存放结果;
    3. return;
    4. }

    回溯搜索的遍历过程

    上文中我们说回溯的树的宽度取决于元素个数,回溯深度取决于递归深度,如图所示

    回溯算法遍历的伪代码如下

    1. for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    2. 处理节点;
    3. backtracking(路径,选择列表); // 递归
    4. 回溯,撤销处理结果
    5. }

    注意这里的撤销,假设我们要求1234中size为2的组合,有 12 13....这里假设我们第一个节点是12,这里我们要得到13就得将2pop出去,也就是我们回溯撤销的过程.

    回溯模板(全)

    1. void backtracking(参数) {
    2. if (终止条件) {
    3. 存放结果;
    4. return;
    5. }
    6. for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    7. 处理节点;
    8. backtracking(路径,选择列表); // 递归
    9. 回溯,撤销处理结果
    10. }
    11. }

     

    2.经典例题 :LeetCode T77 组合问题

    题目链接:77. 组合 - 力扣(LeetCode)

    题目思路:

    我们说递归有三部曲,这里我们回溯也有三部曲,这里我们首先定义一个path变量,来存放我们每条路径上的结果,因为这里我们可以将回溯过程想象成n叉树,所以叶子结点的结果也可以想象成路径的结果,然后定义一个result数组来存放所有路径的集合,这里我们定义为全局变量,下面函数实现中直接操作即可.

    1. List path = new ArrayList<>();
    2. List> result = new ArrayList<>();

    我们以1234中排列2个数字,即n = 4,k = 2为例,画出如下图

    回溯三部曲

    1.确定函数参数和返回值

    这里n和k肯定是需要的,还有我们需要一个参数就是从哪里开始,于是我们定义一个变量startIndex来记录每次递归开始的逻辑,比如第一次从1开始,第二次从2开始

    public void backtracking(int n, int k,int startIndex)

    2.确定终止条件

    这里的终止条也是收割结果的时候,我们发现树的叶子节点就是我们所要的结果,我们写出如下代码 

    1. //终止条件
    2. if(path.size() == k)
    3. {
    4. result.add(new ArrayList<>(path));
    5. return;
    6. }

    3.确定一次递归(回溯)过程

    这里我们按照上文所说就是我们for循环该登场了,这个时候我们的循环就得从startIndex开始到n结束,里面需要做的事情就是path.add元素,再进行backtracking,最后得pop元素进行一次回溯的过程,这里我们可以想象假设上面这个1234的例子,这里我收集了12这个例子,我想收获13这个结果是不是得将2弹出再将3进行添加呀,下面是代码演示

    1. //for循环
    2. for(int i = startIndex;i<=n;i++)
    3. {
    4. path.add(i);
    5. backtracking(n,k,i+1);
    6. path.remove(path.size()-1);
    7. }

    题目代码:

    1. class Solution {
    2. List path = new ArrayList<>();
    3. List> result = new ArrayList<>();
    4. public List> combine(int n, int k) {
    5. backtracking(n,k,1);
    6. return result;
    7. }
    8. public void backtracking(int n, int k,int startIndex)
    9. {
    10. //终止条件
    11. if(path.size() == k)
    12. {
    13. result.add(new ArrayList<>(path));
    14. return;
    15. }
    16. //for循环
    17. for(int i = startIndex;i<=n;i++)
    18. {
    19. path.add(i);
    20. backtracking(n,k,i+1);
    21. path.remove(path.size()-1);
    22. }
    23. }
    24. }

    代码优化

    还是以上面1234举例,这里我们可以进行一次剪枝,假设我们n = 4,k = 4我们就会发现startIndex取2后面的值就毫无意义了,这里我们就可以对这种情况剪枝,如果后面的元素不足以我构成我们的一次正确的结果,就不要去遍历它了

    优化过程如下:

    1. 已经选择的元素个数:path.size();

    2. 还需要的元素个数为: k - path.size();

    3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

    为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

    举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2,这里都是合理的

    只需修改一下for循环的区间即可

    for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)

     

  • 相关阅读:
    JavaMail连接Office 365使用XOAUTH2身份认证
    [ACTF2020 新生赛] Include1
    对工厂模式一次感悟
    Kraft模式下Kafka脚本的使用
    线性表操作集锦(顺序表,链表,栈,队列)
    探索NLP中的核心架构:编码器与解码器的区别
    微软 SQL 服务器被黑,带宽遭到破坏
    如何学习存储性系统
    音频基础知识
    springboot的商品设计热销排行实现
  • 原文地址:https://blog.csdn.net/qiuqiushuibx/article/details/133862585