当前位置: 首页 > article >正文

DFS+回溯+剪枝(深度优先搜索)——搜索算法

        DFS也就是深度优先搜索,比如二叉树的前,中,后序遍历都属于DFS。其本质是递归,要学好DFS首先需要掌握递归。接下来咱们就一起来学习DFS涉及的算法。

一、递归

1.什么是递归?

递归可以这样理解把它拆分出来,两个字,“递”和“归”
递推这就需要找到递推公式
回归需要找到回归条件,递推过程逐渐逼近回归条件

直白一点来说就是,一个函数自己调用自己的情况,当然一定是要能够返回的。

二叉树的遍历,快排,归并中都用到了递归。

2.什么时候使用递归?

        满足以下条件通常都能使用递归解决:在主问题中能找到相同的子问题,在子问题中又能找到相同的子问题。

        注意:递归过程,也就是在前一个函数没有销毁的时候调用下一个相同函数,调用函数就需要开辟函数栈帧,会占用大量空间,所以递归层数太多会导致栈溢出,解决不了问题。

        缺点:递归算法会占用大量内存,有栈溢出的风险,在实际开发中要尽量减少递归算法的使用。

        优点:递归算法简单明了,容易被想到,代码也是非常的好写。

3.如何理解递归?

        在开始学递归时还是需要去分析递归展开细节图的,这个可以让我们理解递归的工作原理,理解为什么递归能解决问题。当我们在逻辑上有了自洽后。就再也不要去管递归展开图,如果一味地去纠结递归展开图,只会让我们越来越晕。

        相反,我们需要从宏观的角度看待递归问题,把递归函数看作一个黑盒并且相信这个黑盒一定能够帮我们完成任务。

4.如何写好递归?

写好递归只需要做好下面这几步:

  • 1.找到相同的子问题 --> 解决函数头的设计。
  • 2.只关心某个子问题是如何解决的 --> 解决函数体的书写。
  • 3.处理递归函数的出口 --> 返回值的确定。

        我们可以知道相同的子问题中的“相同”指的是逻辑相同,而不同的只有操作对象,所以在设计函数头的参数列表时只需要让这些不同的操作对象能够参入即可。

所以我们做这样一个函数头:

void _hanota(vector<int>& A, vector<int>& B, vector<int>& C,int n);

表示把A柱中n个盘子移动到C柱,B是辅助用的柱子。

        提示:这里大家可能会有一个疑惑,为什么传入的参数是几个盘子,而不是哪几个盘子。其实是因为游戏规则本来就是只能从上往下依次从柱子取盘。 所以知道要取一个盘子也就能确定哪几个盘子,它们是一对一的关系。

单个子问题的解决我放在代码中讲解,如下:

class Solution {
public:void _hanota(vector<int>& A, vector<int>& B, vector<int>& C,int n){if(n==1)//只需要移动一个盘的时候直接操作{C.push_back(A.back());A.pop_back();return;}//先把A中n-1个盘移动到B中_hanota(A,C,B,n-1);//在把剩下一个盘移动到C中C.push_back(A.back());A.pop_back();//最后再把B中的n-1个盘移动到C中_hanota(B,A,C,n-1);}void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {//题目通过的参数列表无法满足我们的需求,重写一个函数来解决。_hanota(A,B,C,A.size());}
};

二、记忆化搜索(记忆递归)

通过下面这个题我会引出记忆化搜索。

以上是一个爬楼梯问题,我们通过找规律来解决问题。

楼顶数:1        2        3        4        5        6

方法数:1        2        3        5        8        13

通过观察发现楼顶数x与方法数F( x )的关系为

出现这样一个递推公式我们第一想到的就是递归来实现。

1.递归代码:

int F(int n)
{if(n<=2) return n;else return F(n-1)+F(n-2);
}

注意:这里为了方便说明问题函数名我直接使用F,这和原题提供的函数名不一样。

下面是对以上代码的递归展开图进行剖析:

红线为递推过程,绿线为回归过程。

接下来是复杂度分析

时间复杂度为O(2^n),空间复杂度为O(n)

        通过观察我们发现出现很多重复计算的地方(图中画圈颜色相同的地方)如果减少这些重复计算的地方那么效率会提高很多。为了解决这个问题,我们想象一下,把每次计算的数据存起来,下次用到的时候就不用计算,直接返回。而这就是记忆递归。

2.记忆递归

int arr[46]={0};//通过题目确定数据范围
F(int n)
{if(n<=2) return n;if(arr[n]!=0) return arr[n];else return arr[n]=F(n-1)+F(n-2);
}

        创建一个数组并初始值为零,把每次返回的值存在数组里,这样可以避免重复计算,判断a[n]为非0则直接返回。时间复杂度为O(n),空间复杂度为O(n)。

通常能使用记忆递归解决的问题都能转化为动态规划,如下:

3.动态规划

int F(int n){int dp[46]={0};dp[1]=1,dp[2]=2;for(int i=3;i<n+1;i++)dp[i]=dp[i-1]+dp[i-2];return dp[n];
}

        把1到n,每个楼顶对应的方法数存入数组中,用前两个来计算后一个,直到推到n,此方法相比以上方法,减少了递归带来的内存申请,时间复杂度为O(n),空间复杂度为O(1)。

好题推荐:329. 矩阵中的最长递增路径 - 力扣(LeetCode)

三、回溯

        回溯又叫作“恢复现场”,它是基于递归的一种算法,是为了解决搜索时路径之间的信息互相干扰的问题。如下:

回溯的具体用法,我们来从下面这个题中感受。 

        在做搜索题的时候最重要的莫过于就是决策树的设计。如果决策树做得清晰明了,那么代码也就好写了。

        什么是决策树?在搜索过程中它抽象出来的必定是一棵树形结构,而这棵树是如何展开的,我们在设计展开逻辑的过程,也就是在做一颗决策树。如下:

class Solution {
public:vector<vector<int>> ret;//统计结果vector<int> path;//记录路径vector<vector<int>> subsets(vector<int>& nums){dfs(nums,0);return ret;}void dfs(vector<int>& nums,int pos){if(pos==nums.size())//即到达叶子节点{ret.push_back(path);return;}//不选该元素直接进入下一元素的选择dfs(nums,pos+1);//选择该元素并进入下一元素的选择path.push_back(nums[pos]);dfs(nums,pos+1);//函数退出之前先把该层的数据清除(回溯)path.pop_back();}
};

其实这里回溯思想就一句代码,即path.pop_back()。回溯就这么简单。

我们看一看没有回溯的决策树

这里以叶子节点3开始画回归路线蓝线:回归红线:递推

        我们可以看到如果不把[3]这一节点的信息清除的话它会把信息带到上一层,然后一直往下带,每一节点都不恢复现场,就会使每个节点都带上一个信息往回传,导致结果错误。所以回溯算法在很多场景都是至关重要的。

当然决策树并不是唯一的,每个人画的可能都不一样,比如还可以这样:

不同的决策树代码也是不同的,如下:

class Solution
{
public:vector<vector<int>> ret;vector<int> path;vector<vector<int>> subsets(vector<int>& nums){_subsets(nums,0);return ret;}void _subsets(const vector<int>& nums,int pos){ret.push_back(path);for(int i=pos;i<nums.size();i++){path.push_back(nums[i]);_subsets(nums,i+1);path.pop_back();//恢复现场}}
};

四、剪枝

        剪枝可以这么理解,如果我们已知某条枝干没有正确答案或某条枝干是错误的,那么我们就不进行搜索,这样可以减少不必要的搜索,提高效率。具体我们可以从题中感受。

这个题放在小学数学就是一个画树状图的题,我们直接开始吧。 

        如上这颗决策树,在一条路径中如果一个元素选过一次,那么下次就不能再选,需要把它剪掉。我们可以使用一个哈希表来记录某个元素是否出现过。但在该过程中同样需要注意“恢复现场”。

class Solution {
public:vector<int> path;vector<vector<int>> ret;int n;vector<vector<int>> permute(vector<int>& nums){n = nums.size();vector<bool> hash(n);dfs(nums,hash);return ret;}void dfs(vector<int>& nums,vector<bool>& hash){if(path.size()==n){ret.push_back(path);return;}for(int i=0;i<n;i++){if(hash[i]) continue;//剪枝hash[i]=true;path.push_back(nums[i]);dfs(nums,hash);hash[i]=false;path.pop_back();}}
};

同样的这里剪枝就一句代码,即 if(hash[i]) continue。剪枝就这么简单。 

五、综合试题 

1.N皇后

首先我们还是一样的试着把决策树画出来,如下: 

这样我们可以知道这个题解题框架。接下来就是处理如何剪枝的问题。

        题目要求一个棋子的横排,竖排,斜对角, 反斜对角都不能有其他棋子,那么这就好办,只需要使用4个哈希表来记录这些位置是否已有棋子,如果有那就不能放,直到遍历完所以格子还是无法将棋子放入,则该条路径行不通。

class Solution {
public:vector<vector<string>> ret;vector<string> path;vector<bool>  row,col,bias1,bias2;vector<vector<string>> solveNQueens(int n){row.resize(n,false),col.resize(n,false);bias1.resize(2*n-1,false),bias2.resize(2*n-1,false);for(int i=0;i<n;i++) path.push_back(string(n,'.'));dfs(0,n);return ret;}void dfs(int pos,int n){if(pos==n){ret.push_back(path);return;}for(int j=0;j<n;j++){//剪枝if(row[pos]||col[j]||bias1[pos+j]||bias2[n-pos+j-1]) continue;row[pos]=col[j]=bias1[pos+j]=bias2[n-pos+j-1]=true;path[pos][j]='Q';dfs(pos+1,n);//恢复现场(回溯)row[pos]=col[j]=bias1[pos+j]=bias2[n-pos+j-1]=false;path[pos][j]='.';}}
};

2.解数独 

        同样我们需要画出决策树,我们可以直接暴力搜索每一个空缺的位置,再在每一个空位暴力枚举每一个数字即可。如下:

        在此过程中我们需要注意剪枝与回溯的问题,为了检查数字的合法性,还需要我们记录每一行,每一列,每个3*3小方格中的某个数字是否出现过。所以需要3个哈希表。

由题可知行数列数都是固定的,为9行9列。

        所以可以用   bool row[9][10]     bool col[9][10]来作为哈希表记录某一行的某个数字是否出现过。

使用bool hash[3][3][10],来作为哈希表记录某一个3*3宫格的某个数字是否出现过。

代码示例:

class Solution
{
public:bool row[9][10],col[9][10],hash[3][3][10];void solveSudoku(vector<vector<char>>& board){//初始化哈希表for(int i=0;i<9;i++){for(int j=0;j<9;j++){if(board[i][j]=='.') continue;int key=board[i][j]-'0';row[i][key]=col[j][key]=hash[i/3][j/3][key]=true;}}dfs(board);}bool dfs(vector<vector<char>>& board){for(int i=0;i<9;i++){for(int j=0;j<9;j++){if(board[i][j]!='.') continue;for(int key=1;key<=9;key++){if(row[i][key]||col[j][key]||hash[i/3][j/3][key]) continue;//剪枝board[i][j]=key+'0';row[i][key]=col[j][key]=hash[i/3][j/3][key]=true;if(dfs(board)) return true;//剪枝//恢复现场board[i][j]='.';row[i][key]=col[j][key]=hash[i/3][j/3][key]=false;}return false;}}return true;}
};

好题推荐:

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

22. 括号生成 - 力扣(LeetCode)

1219. 黄金矿工 - 力扣(LeetCode)

77. 组合 - 力扣(LeetCode)

相关文章:

DFS+回溯+剪枝(深度优先搜索)——搜索算法

DFS也就是深度优先搜索&#xff0c;比如二叉树的前&#xff0c;中&#xff0c;后序遍历都属于DFS。其本质是递归&#xff0c;要学好DFS首先需要掌握递归。接下来咱们就一起来学习DFS涉及的算法。 一、递归 1.什么是递归&#xff1f; 递归可以这样理解把它拆分出来&#xff0…...

使用PyCharm创建项目以及如何注释代码

创建好项目后会出现如下图所示的画面&#xff0c;我们可以通过在项目文件夹上点击鼠标右键&#xff0c;选择“New”菜单下的“Python File”来创建一个 Python 文件&#xff0c;在给文件命名时建议使用英文字母和下划线的组合&#xff0c;创建好的 Python 文件会自动打开&#…...

ArrayList和LinkedList有什么区别?在什么情况下使用ArrayList更高效?

ArrayList和LinkedList在Java中是两种常用的数据结构&#xff0c;分别基于数组和链表实现。它们在性能、内存使用和适用场景上各有特点。 ArrayList与LinkedList的主要区别 数据结构&#xff1a; ArrayList&#xff1a;基于动态数组实现&#xff0c;元素存储在连续的内存空间…...

Spring MVC 拦截器(Interceptor)与过滤器(Filter)的区别?

1、两者概述 拦截器&#xff08;Interceptor&#xff09;&#xff1a; 只会拦截那些被 Controller 或 RestController 标注的类中的方法处理的请求&#xff0c;也就是那些由 Spring MVC 调度的请求。过滤器&#xff08;Filter&#xff09;&#xff1a; 会拦截所有类型的 HTTP …...

elasticsearch实战应用从入门到高效使用java集成es快速上手

Elasticsearch 因其出色的性能、可扩展性和易用性,成为了处理大规模数据和构建搜索引擎的首选工具。本文将通过一个实际案例,详细讲解如何在 Spring Boot 项目中集成 Elasticsearch,进行数据索引、搜索、聚合分析等操作。 一、Elasticsearch 简介 Elasticsearch 是一个基于…...

Spring Boot 整合 JPA 实现数据持久化

目录 前言 一、JPA 核心概念与实体映射 1. 什么是 JPA&#xff1f; 2. JPA 的主要组件 3. 实体映射 4. 常见的字段映射策略 二、Repository 接口与自定义查询 1. 什么是 Repository 接口&#xff1f; 2. 动态查询方法 3. 自定义查询 4. 分页与排序 三、实战案例&…...

如何优化网站结构以促进快速收录?

本文转自&#xff1a;百万收录网 原文链接&#xff1a;https://www.baiwanshoulu.com/104.html 优化网站结构以促进快速收录&#xff0c;可以从以下几个方面入手&#xff1a; 一、合理规划页面结构 扁平化结构&#xff1a;采用扁平化的网站结构&#xff0c;减少层级&#xf…...

【零基础学Mysql】常用函数讲解,提升数据操作效率的利器

以耳倾听世间繁华&#xff0c;以语表达心中所想 大家好,我是whisperrrr. 前言&#xff1a; 大家好&#xff0c;我是你们的朋友whisrrr。在日常工作中&#xff0c;MySQL作为一款广泛使用的开源关系型数据库&#xff0c;其强大的功能为我们提供了便捷的数据存储和管理手段。而在…...

防火墙安全综合实验

防火墙安全综合实验 一、拓扑信息 二、需求及配置 实验步骤 需求一&#xff1a;根据下表&#xff0c;完成相关配置 设备接口VLAN接口类型SW2GE0/0/2VLAN 10AccessGE0/0/3VLAN 20AccessGE0/0/1VLAN List&#xff1a;10 20Trunk 1、创建vlan10和vlan20 2、将接口划分到对应…...

在Linux上创建虚拟网卡

在 Linux 上创建虚拟网卡可以通过多种方式进行&#xff0c;常见的方式是使用 ip 命令来配置虚拟网卡。以下是一个简单的步骤指南&#xff0c;用于创建虚拟网卡&#xff1a; 步骤 1: 查看现有的网络接口 首先&#xff0c;查看当前网络接口的状态&#xff0c;可以使用以下命令&…...

AWS Savings Plans 监控与分析工具使用指南

一、背景介绍 1.1 什么是 Savings Plans? AWS Savings Plans 是一种灵活的定价模式,通过承诺持续使用一定金额的 AWS 服务来获得折扣价格。它可以帮助用户降低 AWS 使用成本,适用于 EC2、Fargate 和 Lambda 等服务。 1.2 为什么需要监控? 优化成本支出跟踪使用情况评估投…...

中国通信企业协会通信网络安全服务能力评定安全设计与集成服务能力评定三级要求准则...

安全设计与集成服务能力三级是通信网络安全服务能力评定安全设计与集成服务能力评定的最高等级&#xff0c;所需的要求也会更加严苛&#xff0c;不仅要满足安全设计与集成服务二级能力要求的所有条款&#xff0c;还要满足以下要求&#xff1a; 规模与资产要求 1)单位正规编制员…...

github - 使用

注册账户以及创建仓库 要想使用github第一步当然是注册github账号了, github官网地址:https://github.com/。 之后就可以创建仓库了(免费用户只能建公共仓库),Create a New Repository,填好名称后Create,之后会出现一些仓库的配置信息,这也是一个git的简单教程。 Git…...

RabbitMQ 消息顺序性保证

方式一&#xff1a;Consumer设置exclusive 注意条件 作用于basic.consume不支持quorum queue 当同时有A、B两个消费者调用basic.consume方法消费&#xff0c;并将exclusive设置为true时&#xff0c;第二个消费者会抛出异常&#xff1a; com.rabbitmq.client.AlreadyClosedEx…...

DeepSeek R1 简单指南:架构、训练、本地部署和硬件要求

DeepSeek R1 简单指南&#xff1a;架构、训练、本地部署和硬件要求 DeepSeek 的 LLM 推理新方法 DeepSeek 推出了一种创新方法&#xff0c;通过强化学习 (RL) 来提高大型语言模型 (LLM) 的推理能力&#xff0c;其最新论文 DeepSeek-R1 对此进行了详细介绍。这项研究代表了我们…...

1.攻防世界 unserialize3(wakeup()魔术方法、反序列化工作原理)

进入题目页面如下 直接开审 <?php // 定义一个名为 xctf 的类 class xctf {// 声明一个公共属性 $flag&#xff0c;初始值为字符串 111public $flag 111;// 定义一个魔术方法 __wakeup()// 当对象被反序列化时&#xff0c;__wakeup() 方法会自动调用public function __wa…...

麒麟系统编译安装git

有些版本的麒麟系统上没有git&#xff0c;官网又找不到现成的安装包&#xff0c;只好下载编译进行编译安装 1、下载源码 下载源码&#xff0c;地址&#xff1a;https://git-scm.com/downloads/linux。 2、解压 直接鼠标右键解压&#xff0c;或者用命令行&#xff1a; tar …...

Web - CSS3过渡与动画

过渡 基本使用 transition过渡属性是css3浓墨重彩的特性&#xff0c;过渡可以为一个元素在不同样式之间变化自动添加补间动画。 过渡从kIE10开始兼容&#xff0c;移动端兼容良好&#xff0c;网页上的动画特效基本都是由JavaScript定时器实现的&#xff0c;现在逐步改为css3过…...

Git 常见错误与解决方案全指南

&#x1f680; Git 常见错误与解决方案全指南 这份指南涵盖了你在 Git 操作过程中遇到的所有常见错误、问题及其对应的解决方案&#xff0c;确保你在日常开发中能够快速定位问题并高效解决。 &#x1f517; 1. 如何将本地项目上传到 GitHub 仓库&#xff1f; 步骤&#xff1a…...

OpenStack四种创建虚拟机的方式

实例&#xff08;Instances&#xff09;是在云内部运行的虚拟机。您可以从以下来源启动实例&#xff1a; 一、上传到镜像服务的镜像&#xff08;Image&#xff09; 使用已上传到镜像服务的镜像来启动实例。 二、复制到持久化卷的镜像&#xff08;Volume&#xff09; 使用已…...

线上hbase rs 读写请求个数指标重置问题分析

问题描述: 客户想通过调用hbase的jmx接口获取hbase的读写请求个数,以此来分析HBase读写请求每日增量。 但是发现生产,测试多个集群,Hbase服务指标regionserver读写请求个数存在突然下降到0或者大幅度下降情况。 需要排查原因: 某个Region的读写请求数:会发现经常会重置为…...

【R语言】卡方检验

一、定义 卡方检验是用来检验样本观测次数与理论或总体次数之间差异性的推断性统计方法&#xff0c;其原理是比较观测值与理论值之间的差异。两者之间的差异越小&#xff0c;检验的结果越不容易达到显著水平&#xff1b;反之&#xff0c;检验结果越可能达到显著水平。 二、用…...

2025.2.9机器学习笔记:PINN文献阅读

2025.2.9周报 文献阅读题目信息摘要Abstract创新点网络架构实验结论缺点以及后续展望 文献阅读 题目信息 题目&#xff1a; GPT-PINN:Generative Pre-Trained Physics-Informed Neural Networks toward non-intrusive Meta-learning of parametric PDEs期刊&#xff1a; Fini…...

c语言:取绝对值

假设我们有一个 long 类型的变量 l&#xff0c;我们希望恢复其绝对值。以下是两种方法的对比&#xff1a; 方法1&#xff1a;使用条件语句 这个很好理解&#xff0c;负数时取负运算 &#xff0c;用于数值的符号反转。 long abs_value(long l) {if (l < 0) {return -l;} e…...

JVM(Java 虚拟机)

Java语言的解释性和编译性&#xff08;通过JVM 的执行引擎&#xff09; Java 代码&#xff08;.java 文件&#xff09;要先使用 javac 编译器编译为 .class 文件&#xff08;字节码&#xff09;&#xff0c;紧接着再通过JVM 的执行引擎&#xff08;Execution Engine&#xff09…...

利用二分法进行 SQL 盲注

什么是sql注入&#xff1f; SQL 注入&#xff08;SQL Injection&#xff09;是一种常见的 Web 安全漏洞&#xff0c;攻击者可以通过构造恶意 SQL 语句来访问数据库中的敏感信息。在某些情况下&#xff0c;服务器不会直接返回查询结果&#xff0c;而是通过布尔值&#xff08;Tr…...

大模型数据集全面整理:444个数据集下载地址

本文针对Datasets for Large Language Models: A Comprehensive Survey 中的 444 个数据集&#xff08;涵盖8种语言类别和32个领域&#xff09;进行完整下载地址整理收集。 2024-02-28&#xff0c;由杨刘、曹家欢、刘崇宇、丁凯、金连文等作者编写&#xff0c;深入探讨了大型语…...

Ubuntu 下 nginx-1.24.0 源码分析 ngx_tm_t 类型

src\os\unix\ngx_time.h 中 typedef struct tm ngx_tm_t; tm 是 C 标准库中定义的一个结构体&#xff0c;通常用于表示日期和时间的信息。它通常定义在 <time.h> 头文件中 struct tm {int tm_sec; /* 秒&#xff0c;范围 0-59 */int tm_min; /* …...

Linux 创建进程 fork()、vfork() 与进程管理

Linux 创建进程 fork、vfork、进程管理 一、Linux的0号、1号、2号进程二、Linux的进程标识三、fork() 函数1、基本概念2、函数特点3、用法以及应用场景&#xff08;1&#xff09;父子进程执行不同的代码&#xff08;2&#xff09;进程执行另一个程序 4、工作原理 四、vfork() 函…...

2025web寒假作业二

一、整体功能概述 该代码构建了一个简单的后台管理系统界面&#xff0c;主要包含左侧导航栏和右侧内容区域。左侧导航栏有 logo、管理员头像、导航菜单和安全退出按钮&#xff1b;右侧内容区域包括页头、用户信息管理内容&#xff08;含搜索框和用户数据表格&#xff09;以及页…...