算法通关村第十八关青铜挑战——透析回溯的模板

news2024/11/14 20:49:51

大家好,我是怒码少年小码。

回溯是最重要的算法思想之一,主要解决一些暴力枚举也搞不定的问题(组合、子集、分割、排列、棋盘等等)。性能并不高,但是哪些暴力枚举都无法ko的问题能解出来就可以了🤣。

这一篇,让我们来看看回溯是个什么玩意儿😎。

回溯思想

定义

是一个种基于深度优先搜索的思想,在搜索过程中通过剪枝操作来减少搜索空间,从而找到问题的解。

回溯可以理解为递归的拓展,而代码结构又特别像深度遍历N叉树,因此只要知道递归,理解回溯并不难。

模板

回溯比其他思想好理解的一点就是它是有模板的,下面是伪代码:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择本层集合中元素(画成树,就是树节点孩子的大小)){
        处理节点;
        backtracking();
        回溯,撤销处理结果;
    }
}

本文我们只干一件事——分析回溯的模板🫡。

从树的遍历开始

透析一种基于深度优先搜素的思想,首先我们需要回顾一下树的遍历。二叉树中,前序遍历的代码如下:

class TreeNode{
   int val;
   TreeNode left;
   TreeNode right;
}
void treeDFS(TreeNode root) {
    if (root == null)
        return;
    System.out.println(root.val);
    treeDFS(root.left);
    treeDFS(root.right);  
}

如果是n叉树,就会变成:

class TreeNode{
   int val;
   List<TreeNode> nodes;
}
public static void treeDFS(TreeNode root) {
    //递归必须要有终止条件
    if (root == null){
     return;
    }
    //处理节点
    System.out.println(root.val);
 
    //通过循环,分别遍历N个子树
    for (int i = 1; i <= nodes.length; i++) {
        treeDFS("第i个子节点");
    }
}

因为是n叉树,所以没办法再用leftright表示分支了,这里用了一个List。观察上面的代码是不是和回溯的模板特别像!

暴力枚举解决不了的原因

看一下这道题,LeetCode 77:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

示例 :

  • 输入:n = 4, k = 2
  • 输出:
    [
    [2,4],
    [3,4],
    [2,3],
    [1,2],
    [1,3],
    [1,4],
    ]

从4个数中选择2个:

  • 先取1,则有[1,2],[1,3],[1,4]。
  • 然后取2,因为1已经取过了,不再取,则有[2,3],[2,4]。
  • 再取一个3,因为1和2都取过了,不再取,则有[3,4]。
  • 再取4,因为1,2,3都已经取过了,直接返回null。
  • 所以最终是[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]。

写成代码双层循环轻松搞定:

int n = 4;
for (int i = 1; i <= n; i++) {
     for (int j = i + 1; j <= n; j++) {
       System.out.println(i + " " + j);
     }
}

但是如果n和k变得很大呢?取2个用2个循环,取k个要套多少层循环?显然暴力枚举就不行了,这就是组合问题。

回溯=递归+局部枚举+放下前任

继续以LeetCode 77为例子,图示枚举n=4,k=2的情况:

  • 从第一层到第二层的分支有四个,分别表示可以取1,2,3,4。
  • 从左向右取数,取过的数,不在重复取。
  • 第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4]。

以此类推:

n=5,k=3的情况:

分析规律

图中每次访问到一次叶子节点(红框标记处),就找到一个结果。虽然最后一个是空,但是不影响结果。这相当于只需要把从根节点开始每次选择的内容(分支)达到叶子节点时,将其收集起来就是想要的结果。

元素个数n相当于树的宽度(横向),k相当于树的深度(纵向)。所以我们说回溯算法就是一纵一横而已。再分析,我们还发现几个规律:

  • 我们每次选择都是从类似{1,2,3,4},{1,2,3,4,5}这样的序列中一个个选的,这就是局部枚举,而且越往后枚举范围越小。
  • 枚举时,我们就是简单的暴力测试而已,一个个验证,能否满足要求,从上图可以看到,这就是N叉树遍历的过程,因此两者代码也必然很像。
  • 我们再看上图中红色大框起来的部分,这个部分的执行过程与n=4,k=2的处理过程完全一致,很明显这是个可以递归的子结构。

为什么要手动撤销(放下前任)

收集每个结果不是针对叶子结点的,而是针对树枝的,比如最上层我们首先选了1,下层如果选2,结果就是{1,2},如果下层选了3,结果就是{1,3},依次类推。

观察图片,我可以在得到{1,2}之后将2撤掉,再继续取3,这样就得到了{1,3},同理可以得到{1,4},之后当前层就没有了,我们可以将1撤销,继续从最上层取2继续进行。

手动撤销对应的代码操作:

先将第一个结果放在临时列表path里,得到第一个结果{1,2}之后就将path里的内容放进结果列表resultList中,之后,将path里的2撤销掉, 继续寻找下一个结果{1.3},然后继续将path放入resultLit,然后再撤销继续找。

完整代码:

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> resultList = new ArrayList<>();
    if (k <= 0 || n < k) {
        return resultList;
    }
    //用户返回结果
    Deque<Integer> path = new ArrayDeque<>();
    dfs(n, k, 1, path, res);
    return res;
}
 
public void dfs(int n, int k, int startIndex, Deque<Integer> path, List<List<Integer>> resultList) {
    //递归终止条件是:path 的长度等于 k
    if (path.size() == k) {
        resultList.add(new ArrayList<>(path));
        return;
    }
 
    //针对一个结点,遍历可能的搜索起点,其实就是枚举
    for (int i = startIndex; i <= n; i++) {
        //向路径变量里添加一个数,就是上图中的一个树枝的值
        path.addLast(i);
        //搜索起点要加1是为了缩小范围,下一轮递归做准备,因为不允许出现重复的元素
        dfs(n, k, i + 1, path, resultList);
        //递归之后需要做相同操作的逆向操作,具体后面继续解释
        path.removeLast();
    }
}

热身小题

输出二叉树的所有路径

LeetCode 257:给你一个二叉树的根节点 root ,按任意顺序,返回所有从根节点到叶子节点的路径。

分析:可以得出有几个叶子结点就有几条路径。深度优先搜索就是从根节点开始一直找到叶子结点,我们这里可以先判断当前节点是不是叶子结点,再决定是不是向下走,如果是叶子结点,我们就增加一条路径。

从回溯的角度来看得到第一条路径125之后怎么找到第二条路径13,这里很明显就是先将5撤掉,发现还是不行,再撤掉2,然后接着递归就可以了。

class Solution {
    List<String> ans = new ArrayList<>();
    public List<String> binaryTreePaths(TreeNode root) {
        dfs(root,new ArrayList<>());
        return ans;
    }

    private void dfs(TreeNode root,List<Integer> temp){
        if(root == null){
            return ;
        }

        temp.add(root.val);
        //如果是叶子结点记录结果
        if(root.left == null && root.right == null){
            ans.add(getPathString(temp));
        }
        dfs(root.left,temp);
        dfs(root.right,temp);
        temp.remove(temp.size()-1);
    }
    //拼接结果
    private String getPathString(List<Integer> temp){
        StringBuilder sb = new StringBuilder();
        sb.append(temp.get(0));
        for(int i = 1;i < temp.size();++i){
            sb.append("->").append(temp.get(i));
        }
        return sb.toString();
    }
}

这是力扣上的简单题,怎么说呢?不知道大家做这道题怎么样,反正我是真的一点也没破防😎。

路径总和的问题

LeetCode 113:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

分析:我们发现根节点是5,因此只要从左侧或者右侧找到targetSum是17的即可。

  • 看左子树,根值为4,那只要从node(4)的左右子树中找targetSum是13
  • node(11)时,我们需要再找和为2的子链路,显然此时node(7)已经超了,要将node(7)给移除掉,继续访问node(2)。左子树遍历完毕
  • 看根结点的右子树,要找targetSum为17的链路,方式与上面的一致。完整代码就是:
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
    LinkedList<Integer> path = new LinkedList<>();
    dfs(root,targetSum,path);
    return res;
}
public void dfs(TreeNode root,int targetSum,LinkedList<Integer> path){
    if(root == null){
        return ;
    }
    targetSum -= root.val;
    path.add(root.val);
    if(targetSum == 0 && root.left == null && root.right == null){
        res.add(new LinkedList(path));
    }
    dfs(root.left,targetSum,path);
    dfs(root.right,targetSum,path);
    path.removeLast();
}

总的来说:

  1. 首先,我们传入一个二叉树的根节点和一个目标和。我们创建一个二维列表 res 用于保存结果。
  2. 接着,我们创建一个 LinkedList 类型的 path 用于保存当前路径。使用深度优先搜索(DFS)的方式遍历二叉树,具体实现在 dfs 函数中。在 dfs 函数中,首先我们判断当前节点是否为空。如果为空,则直接返回。
  3. 然后,我们将目标和减去当前节点值,并将当前节点的值添加到路径中。
  4. 进一步,我们判断是否满足目标和为 0 且当前节点为叶子节点(即没有左右子树)。如果满足条件,我们将当前路径添加到结果列表 res 中。
  5. 接下来,我们分别递归地遍历当前节点的左子树和右子树,注意此时目标和和路径都已经更新。
  6. 最后,我们移除路径中的最后一个值,以便继续向上寻找其他路径。
  7. 最终返回保存有所有符合条件路径的结果列表 res。

END

啊~~知识的力量,🧠脑子好疼🥲。下一篇我将专门介绍回溯思想的经典算法题目和应用场景,这篇就当是个入门,知道什么是回溯就行了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1272549.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32-Web-Server编程- 通过 Highcharts 创建图表(Chart)实时显示设备信息

ESP32-Web-Server编程- 通过 Highcharts 创建图表&#xff08;Chart&#xff09;实时显示设备信息 概述 上节讲述了通过 Server-Sent Events&#xff08;以下简称 SSE&#xff09; 实现在网页实时更新 ESP32 Web 服务器的传感器数据&#xff0c;并通过表格显示传感器的数据。…

EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测

EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测 目录 EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【EI级】Matlab实现TCN-BiLSTM-Multihead-…

MySQL之JDBC

&#x1f495;"我像离家的孤儿,回到了母亲的怀抱,恢复了青春。"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;MySQL之JDBC 一.什么是JDBC? JDBC编程就是通过Java 代码来操纵数据库 数据库编程&#xff0c; 需要数据库服务器提供一些API供程序…

C语言每日一题(42)删除链表的倒数第N个结点

力扣网 19 删除链表的倒数第N个结点 题目描述 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;head …

Linux下查看目录大小

查看目录大小 Linux下查看当前目录大小&#xff0c;可用一下命令&#xff1a; du -h --max-depth1它会从下到大的显示文件的大小。

Python字符串大小写转换的函数及用法

Python字符串由内建的str类代表&#xff0c;那么str 类包含哪些方法呢&#xff1f;Python 非常方便&#xff0c;它甚至不需要用户查询文档&#xff0c;Python 是“自带文档”的。 这里需要读者简单掌握两个帮助函数&#xff1a; dir()&#xff1a;列出指定类或模块包含的全部…

深入解析 Python 中 Parsel 的两种数据提取方式

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在网络爬虫的世界中&#xff0c;数据提取是至关重要的一环。Python 提供了许多强大的工具&#xff0c;其中之一就是 parsel 库&#xff0c;专门用于解析和提取 HTML 或 XML 数据。本篇博客将深入探讨 parsel 中两…

8. 队列

队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义&#xff0c;队列模拟了排队现象&#xff0c;即新来的人不断加入队列的尾部&#xff0c;而位于队列头部的人逐个离开。 如下图所示&#xff0c;我们将队列的头部称为“队首”&#xff0c;尾部称为“队尾”&#xff…

聚硫醇固化剂Capure3-800含巯基-SH环氧固化剂TDS说明书

聚硫醇固化剂Capure3-800含巯基-SH环氧固化剂TDS说明书

【程序员的自我修养03】深入了解ELF文件格式

绪论 大家好&#xff0c;欢迎来到【程序员的自我修养】专栏。正如其专栏名&#xff0c;本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度&#xff0c;我会尽自己最大的努力&#xff0c;争取…

Python 异常处理(try except)

文章目录 1 概述1.1 异常示例 2 异常处理2.1 捕获异常 try except2.2 抛出异常 raise 3 异常类型3.1 内置异常3.2 自定义异常 1 概述 1.1 异常示例 异常&#xff1a;程序执行中出现错误&#xff0c;若不处理&#xff0c;则程序终止 示例代码&#xff1a; v 6 / 0 # 除数不…

滴水逆向三期笔记与作业——02C语言——07 多维数组

OneNote防丢 多维数组 一、二维数组的初始化1.1 二维数组初始化1.2 给定部分值1.3 二维数组省略第二层的{}1.4 省略长度1.5 多维数组寻址1.6 作业 一、二维数组的初始化 1.1 二维数组初始化 代码1 int arr[3][4] { {1,2,3,4}, {5,6,7,8}, {9,7,6,5} }; 代码2 int arr[3*4…

科普:什么是合同生命周期管理?

在当前&#xff0c;企业面临自身转型升级与行业数字建设的挑战&#xff0c;急需一种系统化的解决方案&#xff0c;帮助企业在自身运作方面实现降本增效&#xff0c;为拓展业务获得发展提供助力&#xff0c;而合同生命周期管理在其中就扮演着十分重要的角色。 一、合同生命周期…

超详细!Opencv人脸识别!附源码!

一、新建环境 注意&#xff01;&#xff01;确定后需要关闭项目&#xff0c;重新打开&#xff0c;终端的环境才会变化&#xff01;&#xff01; 二、下载安装包&#xff08;只需要3个即可&#xff09; 1. 下载dlib包 pip install dlib-19.19.0-cp38-cp38-win_amd64.whl.whl …

数据结构学习笔记——二叉树的遍历和链式存储代码实现二叉树

目录 一、二叉树的遍历&#xff08;一&#xff09;二叉树的先序遍历&#xff08;DLR&#xff09;&#xff08;二&#xff09;二叉树的中序遍历&#xff08;LDR&#xff09;&#xff08;三&#xff09;二叉树的后序遍历&#xff08;LRD&#xff09;&#xff08;四&#xff09;二…

修复电脑中缺失的VCRUNTIME140.dll文件的5个有效方法

vcruntime140.dll丢失5个修复方法与vcruntime140.dll是什么以及丢失对电脑的影响 引言&#xff1a; 在日常使用电脑的过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“vcruntime140.dll丢失”。那么&#xff0c;什么是vcruntime140.dll&#xff1f;它…

虚幻学习笔记1—给UI添加动画

一、前言 本文所使用的虚幻版本为5.3.2&#xff0c;之前工作都是用unity&#xff0c;做这类效果用的最多的是一个DoTween的插件&#xff0c;在虚幻中都内置集成了这这种效果制作。 图1.1 UI动画 二、过程 1、首先&#xff0c;在诸如按钮、图像等可交互控件中选中&#xff0c;如…

【驱动】串口驱动分析(四)-串口编程和调试方法

串口调试 串口调试主要有 根据/proc系统信息确认串口状态&#xff0c;stty命令&#xff0c;编程调试 三种调试方法&#xff0c;下面我们分别具体介绍下。 根据设备节点确认串口是否正常 系统上电时&#xff0c;默认会使能串口&#xff0c;我们可以通过dmesg | grep ttyS 查看…

2023 年 IntelliJ IDEA下载、安装教程,附详细图文

大家好&#xff0c;今天为大家带来的是 2023年 IntelliJ IDEA 下载、安装教程&#xff0c;超详细的图文教程&#xff0c;亲测可用。 文章目录 1 IDEA 下载2 IDEA 安装3 IDEA 使用4 快捷键新手必须掌握&#xff1a;Ctrl&#xff1a;Alt&#xff1a;Shift&#xff1a;Ctrl Alt&a…

mysql:免费的GUI客户端工具推荐并介绍常用的操作

给大家推荐几个常用的 mysql 数据库客户端 sequel-pro sequel-ace 官网下载地址 免费 sequel-ace 可以理解为 Sequel Pro 的升级版&#xff0c;由于Sequel Pro官方不维护了&#xff0c;特别是对 MySQL 8.0 支持不好&#xff0c;所以现在由社区维护了新分支 sequel-ace&#x…