DFS+回溯+剪枝(深度优先搜索)——搜索算法
目录
一、递归
1.什么是递归?
2.什么时候使用递归?
3.如何理解递归?
4.如何写好递归?
二、记忆化搜索(记忆递归)
三、回溯
四、剪枝
五、综合试题
1.N皇后
2.解数独
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+回溯+剪枝(深度优先搜索)——搜索算法
目录 一、递归 1.什么是递归? 2.什么时候使用递归? 3.如何理解递归? 4.如何写好递归? 二、记忆化搜索(记忆递归) 三、回溯 四、剪枝 五、综合试题 1.N皇后 2.解数独 DFS也就是深度优先搜索&am…...

在cursor/vscode中使用godot C#进行游戏开发
要在 Visual Studio Code(VS Code)中启动 C#Godot 项目,可以按照以下步骤进行配置: 1.安装必要的工具 • 安装 Visual Studio Code:确保你已经安装了最新版本的 VS Code。 • 安装.NET SDK:下载并安装.NET 7.x SDK(…...
vant4 van-list组件的使用
<van-listv-if"joblist && joblist.length > 0"v-model:loading"loading":finished"finished":immediate-check"false"finished-text"没有更多了"load"onLoad">// 加载 const loading ref(fals…...
介绍 Liquibase、Flyway、Talend 和 Apache NiFi:选择适合的工具
在现代软件开发中,尤其是在数据库管理和数据集成方面,选择合适的工具至关重要。本文将介绍四个流行的工具:Liquibase、Flyway、Talend 和 Apache NiFi,分析它们的应用、依赖以及如何选择适合的工具。 1. Liquibase 简介ÿ…...

攻防世界33 catcat-new【文件包含/flask_session伪造】
题目: 点击一只猫猫: 看这个url像是文件包含漏洞,试试 dirsearch扫出来/admin,访问也没成功(--delay 0.1 -t 5) 会的那几招全用不了了哈哈,那就继续看答案 先总结几个知识点 1./etc/passwd&am…...
Git -> Git配置密钥对,并查看公钥
Git密钥对的核心作用 私钥 (id_rsa) 你的数字身份证:存放在本机 ~/.ssh 目录下必须严格保密(类似银行卡密码),不可泄露或共享用于 解密 来自服务器的加密信息 公钥 (id_rsa.pub) 可公开的验证锁:需要上传到 Git 服…...

淘宝订单列表Fragment转场动画卡顿解决方案
如何应对产品形态与产品节奏相对确定情况下转变为『在业务需求与产品形态高度不确定性的情况下,如何实现业务交付时间与交付质量的确定性』。我们希望通过混合架构(Native 业务容器 Weex 2.0)作为未来交易终端架构的重要演进方向,…...

【ESP32指向鼠标】——icm20948与esp32通信
【ESP32指向鼠标】——icm20948与esp32通信 ICM-20948介绍 ICM-20948 是一款由 InvenSense(现为 TDK 的一部分)生产的 9 轴传感器集成电路。它结合了 陀螺仪、加速度计和磁力计。 内置了 DMP(Digital Motion Processor)即负责执…...

Xcode证书密钥导入
证书干嘛用 渠道定期会给xcode证书,用来给ios打包用,证书里面有记录哪些设备可以打包进去。 怎么换证书 先更新密钥 在钥匙串访问中,选择系统。(选登录也行,反正两个都要导入就是了)。 mac中双击所有 .p12 后缀的密钥ÿ…...

Ubuntu安装PgSQL17
参考官网教程,Ubuntu24 apt在线安装Postgres 17 1. 要手动配置 Apt 存储库 # 导入存储库签名密钥: sudo apt install curl ca-certificates sudo install -d /usr/share/postgresql-common/pgdg sudo curl -o /usr/share/postgresql-common/pgdg/apt…...
K8S容器启动提示:0/2 nodes are available: 2 Insufficient cpu.
问题:K8S的容器启动报错0/2 nodes are available: 2 Insufficient cpu. 原因:Pod的资源请求(requests)设置不当:在Kubernetes中,调度器根据Pod的requests字段来决定哪个节点可以运行该Pod。如果一个Pod声明…...

LabVIEW外腔二极管激光器稳频实验
本项目利用LabVIEW软件开发了一个用于外腔二极管激光器稳频实验的系统。系统能够实现激光器频率的稳定控制和实时监测,为激光实验提供了重要支持。 项目背景: 系统解决了外腔二极管激光器频率不稳定的问题,以满足对激光器频率稳定性要求较高…...

笔记6——字典dict(dictionary)
文章目录 字典dict(dictionary)定义特点常用操作1.访问值2.添加键值对3.修改值4.删除键值对5.遍历字典6.合并字典 性能应用场景dict和list的区别 字典dict(dictionary) 以 键 - 值对 (key - value pairs)的形式存储数据 定义 字典使用花括号 {} 来定义,键和值之…...
【MySQL】InnoDB单表访问方法
目录 1、背景2、环境3、访问类型【1】const【2】ref【3】ref_or_null【4】range【5】index【6】all 4、总结 1、背景 mysql通过查询条件查询到结果的过程就叫访问方法,一条查询语句的访问方法有很多种,接下来我们就来讲一下各种访问方法。 2、环境 创…...

APP端网络测试与弱网模拟!
当前APP网络环境比较复杂,网络制式有2G、3G、4G网络,还有越来越多的公共Wi-Fi。不同的网络环境和网络制式的差异,都会对用户使用app造成一定影响。另外,当前app使用场景多变,如进地铁、上公交、进电梯等,使…...

【个人开发】deepseed+Llama-factory 本地数据多卡Lora微调
文章目录 1.背景2.微调方式2.1 关键环境版本信息2.2 步骤2.2.1 下载llama-factory2.2.2 准备数据集2.2.3 微调模式2.2.4 微调脚本 2.3 踩坑经验2.3.1 问题一:ValueError: Undefined dataset xxxx in dataset_info.json.2.3.2 问题二: ValueError: Target…...

Redis7.0八种数据结构底层原理
导读 本文介绍redis应用数据结构与物理存储结构,共八种应用数据结构和 一. 内部数据结构 1. sds sds是redis自己设计的字符串结构有以下特点: jemalloc内存管理预分配冗余空间二进制安全(c原生使用\0作为结尾标识,所以无法直接存储\0)动态计数类型(根据字符串长度动态选择…...
Kafka 高吞吐量的底层技术原理
Kafka 之所以能够实现高吞吐量(每秒百万级消息处理),主要依赖于其底层设计和多项优化技术。以下是 Kafka 实现高吞吐量的关键技术原理: 1. 顺序读写磁盘 Kafka 利用磁盘的顺序读写特性,避免了随机读写的性能瓶颈。 顺…...

CCFCSP第34次认证第一题——矩阵重塑(其一)
第34次认证第一题——矩阵重塑(其一) 官网链接 时间限制: 1.0 秒 空间限制: 512 MiB 相关文件: 题目目录(样例文件) 题目背景 矩阵(二维)的重塑(reshap…...

网络工程师 (35)以太网通道
一、概念与原理 以太网通道,也称为以太端口捆绑、端口聚集或以太链路聚集,是一种将多个物理以太网端口组合成一个逻辑通道的技术。这一技术使得多个端口能够并行工作,共同承担数据传输任务,从而提高了网络的传输能力和可靠性。 二…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...

基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...

Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...