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)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DeepSeek从入门到精通教程PDF清华大学出版

DeepSeek爆火以来&#xff0c;各种应用方式层出不穷&#xff0c;对于很多人来说&#xff0c;还是特别模糊&#xff0c;有种雾里看花水中望月的感觉。 最近&#xff0c;清华大学新闻与传播学院新媒体研究中心&#xff0c;推出了一篇DeepSeek的使用教程&#xff0c;从最基础的是…

idea Ai工具通义灵码,Copilot我的使用方法以及比较

我用过多个idea Ai 编程工具&#xff0c;大约用了1年时间&#xff0c;来体会他们那个好用&#xff0c;以下只是针对我个人的一点分享&#xff0c;不一定对你适用 仅作参考。 介于篇幅原因我觉得能说上好用的 目前只有两个 一个是阿里的通义灵码和Copilot&#xff0c;我用它来干…

C++ Primer sizeof运算符

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

【C++】命名空间

&#x1f31f; Hello&#xff0c;我是egoist2023&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; 目录 背景知识 命名空间(namespace) 为何引入namespace namespace的定义 namespace的使用 背景知识 C的起源要追溯到1979年&#xff0…

(2024|Nature Medicine,生物医学 AI,BiomedGPT)面向多种生物医学任务的通用视觉-语言基础模型

BiomedGPT: A generalist vision–language foundation model for diverse biomedical tasks 目录 1. 摘要 2. 引言 3. 相关研究 3.1 基础模型与通用生物医学 AI 3.2 生物医学 AI 的局限性 3.3 BiomedGPT 的创新点 4. 方法 4.1 架构及表示 4.1.1 模型架构选择 4.1.2 …

使用PyCharm进行Django项目开发环境搭建

如果在PyCharm中创建Django项目 1. 打开PyCharm&#xff0c;选择新建项目 2.左侧选择Django&#xff0c;并设置项目名称 3.查看项目解释器初始配置 4.新建应用程序 执行以下操作之一&#xff1a; 转到工具| 运行manage.py任务或按CtrlAltR 在打开的manage.pystartapp控制台…

AD域控粗略了解

一、前提 转眼大四&#xff0c;目前已入职上饶一公司从事运维工程师&#xff0c;这与我之前干的开发有着很大的差异&#xff0c;也学习到了许多新的知识。今天就写下我对于运维工作中常用的功能——域控的理解。 二、为什么要有域控&#xff0c;即域控的作用 首先我们必须要…

Linux(21)——系统日志

目录 一、系统日志架构&#xff1a; 1、系统日志&#xff1a; 2、日志文件类型&#xff1a; 二、查看 syslog 文件&#xff1a; 1、将事件记录到系统&#xff1a; &#xff08;1&#xff09;syslog 设备&#xff1a; &#xff08;2&#xff09;syslog 优先级&#xff1a…

学习数据结构(6)单链表OJ上

1.移除链表元素 解法一&#xff1a;&#xff08;我的做法&#xff09;在遍历的同时移除&#xff0c;代码写法比较复杂 解法二&#xff1a;创建新的链表&#xff0c;遍历原链表&#xff0c;将非val的节点尾插到新链表&#xff0c;注意&#xff0c;如果原链表结尾是val节点需要将…

第433场周赛:变长子数组求和、最多 K 个元素的子序列的最值之和、粉刷房子 Ⅳ、最多 K 个元素的子数组的最值之和

Q1、变长子数组求和 1、题目描述 给你一个长度为 n 的整数数组 nums 。对于 每个 下标 i&#xff08;0 < i < n&#xff09;&#xff0c;定义对应的子数组 nums[start ... i]&#xff08;start max(0, i - nums[i])&#xff09;。 返回为数组中每个下标定义的子数组中…

CSS 伪类(Pseudo-classes)的详细介绍

CSS 伪类详解与示例 在日常的前端开发中&#xff0c;CSS 伪类可以帮助我们非常精准地选择元素或其特定状态&#xff0c;从而达到丰富页面表现的目的。本文将详细介绍以下伪类的使用&#xff1a; 表单相关伪类 :checked、:disabled、:enabled、:in-range、:invalid、:optional、…

Centos挂载镜像制作本地yum源,并补装图形界面

内网环境centos7.9安装图形页面内网环境制作本地yum源 上传镜像到服务器目录 创建目录并挂载镜像 #创建目录 cd /mnt/ mkdir iso#挂载 mount -o loop ./CentOS-7-x86_64-DVD-2009.iso ./iso #前面镜像所在目录&#xff0c;后面所挂载得目录#检查 [rootlocalhost mnt]# df -h…

大模型推理——MLA实现方案

1.整体流程 先上一张图来整体理解下MLA的计算过程 2.实现代码 import math import torch import torch.nn as nn# rms归一化 class RMSNorm(nn.Module):""""""def __init__(self, hidden_size, eps1e-6):super().__init__()self.weight nn.Pa…

Python截图轻量化工具

一、兼容局限性 这是用Python做的截图工具&#xff0c;不过由于使用了ctypes调用了Windows的API, 同时访问了Windows中"C:/Windows/Cursors/"中的.cur光标样式文件, 这个工具只适用于Windows环境&#xff1b; 如果要提升其跨平台性的话&#xff0c;需要考虑替换cty…

链表(LinkedList) 1

上期内容我们讲述了顺序表&#xff0c;知道了顺序表的底层是一段连续的空间进行存储(数组)&#xff0c;在插入元素或者删除元素需要将顺序表中的元素整体移动&#xff0c;时间复杂度是O(n)&#xff0c;效率比较低。因此&#xff0c;在Java的集合结构中又引入了链表来解决这一问…

SpringAI系列 - 使用LangGPT编写高质量的Prompt

目录 一、LangGPT —— 人人都可编写高质量 Prompt二、快速上手2.1 诗人 三、Role 模板3.1 Role 模板3.2 Role 模板使用步骤3.3 更多例子 四、高级用法4.1 变量4.2 命令4.3 Reminder4.4 条件语句4.5 Json or Yaml 方便程序开发 一、LangGPT —— 人人都可编写高质量 Prompt La…

jupyterLab插件开发

jupyter lab安装、配置&#xff1a; jupyter lab安装、配置教程_容器里装jupyterlab-CSDN博客 『Linux笔记』服务器搭建神器JupyterLab_linux_布衣小张-腾讯云开发者社区 Jupyter Lab | 安装、配置、插件推荐、多用户使用教程-腾讯云开发者社区-腾讯云 jupyterLab插件开发教…

使用LLaMA Factory踩坑记录

前置条件&#xff1a;电脑显卡RTX 4080 问题&#xff1a;LLaMA-Factory在运行的时候&#xff0c;弹出未检测到CUDA的报错信息 结论&#xff1a;出现了以上的报错&#xff0c;主要可以归结于以下两个方面&#xff1a; 1、没有安装GPU版本的pytorch&#xff0c;下载的是CPU版本…

『Apisix进阶篇』结合Consul作服务发现实战演练

文章目录 一、引言二、APISIX与Consul集成2.1 环境准备2.2 配置Consul服务发现2.2.1 修改APISIX配置文件2.2.2 重启APISIX 2.3 在路由中使用Consul服务发现2.3.1 创建路由2.3.2 验证路由 2.4 高级配置2.4.1 服务过滤2.4.2 多数据中心支持 三、总结 &#x1f4e3;读完这篇文章里…

SpringBoot速成(八)登录实战:未登录不能访问 P5-P8

1.登录 package com.itheima.springbootconfigfile.controller;import com.itheima.springbootconfigfile.pojo.Result; import com.itheima.springbootconfigfile.pojo.User; import com.itheima.springbootconfigfile.service.UserService;import com.itheima.springbootco…