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)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Windows 11 重装系统后蓝屏错误:INACCESSIBLE_BOOT_DEVICE 的解决方案

Windows 11 重装系统后蓝屏错误&#xff1a;INACCESSIBLE_BOOT_DEVICE 的解决方案 在安装 Windows 11 后&#xff0c;用户可能会遇到一个令人头疼的问题&#xff1a;蓝屏错误&#xff0c;错误代码为 INACCESSIBLE_BOOT_DEVICE。这个错误通常表示系统无法访问启动设备&#xff…

瑞熙贝通实验室安全综合管理平台更新迭代v4.0产品介绍

随着科研事业的蓬勃发展&#xff0c;科研实验室是高校科研的重要场所 &#xff0c;是培养学生科研能力、进行科学实验、创造科研成果的重要基地。然而&#xff0c;实验室也存在诸多安全隐患&#xff0c;如化学品泄露、火灾、设备故障、中毒、辐射、窒息等&#xff0c;这些都可能…

【读书笔记·VLSI电路设计方法解密】问题46:什么是bug覆盖率

在IC设计项目的验证过程中&#xff0c;功能测试&#xff08;通过使用测试平台&#xff09;有助于定位设计错误或漏洞。这个验证过程有三个阶段&#xff1a;构建和启动测试平台、验证基本测试用例以及验证边界情况。 在前两个阶段&#xff0c;漏洞很容易被检测到&#xff0c;因…

UA-Track:不确定性感知端到端3D多目标跟踪

论文地址&#xff1a;https://arxiv.org/pdf/2406.02147 主页&#xff1a;https://liautoad.github.io/ua-track-website/ 3D多目标跟踪&#xff08;MOT&#xff09;在自动驾驶感知中起着至关重要的作用。最近基于端到端查询的跟踪器可以同时检测和跟踪对象&#xff0c;这在3D …

CSS入门学习笔记(二)

学习视频&#xff1a;https://www.bilibili.com/video/BV1zN2UYoEEo/ 目录 浮动浮动的几种应用效果设置img浮动&#xff0c;去掉空隙设置div重叠&#xff0c;位于上下层多个div水平排列宽度不足时&#xff0c;会自动换行li元素水平排列 浮动的副作用解决副作用——清除浮动方法…

解锁 DeepSeek 模型高效部署密码:蓝耘平台深度剖析与实战应用

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

打家劫舍3

今天和打家讲一下打家劫舍3 题目&#xff1a; 题目链接&#xff1a;337. 打家劫舍 III - 力扣&#xff08;LeetCode&#xff09; 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口&#xff0c;我们称之为root。 除了 root 之外&#xff0c;每栋房子有且只有一个“父“…

redis项目

短信登录 这一块我们会使用redis共享session来实现 商户查询缓存 通过本章节&#xff0c;我们会理解缓存击穿&#xff0c;缓存穿透&#xff0c;缓存雪崩等问题&#xff0c;让小伙伴的对于这些概念的理解不仅仅是停留在概念上&#xff0c;更是能在代码中看到对应的内容 优惠…

每日一题洛谷P5733 【深基6.例1】自动修正c++

#include<iostream> #include<string> using namespace std; int main() {string t;cin >> t;for (int i 0; i < t.length(); i){if (t[i] > a && t[i] < z){t[i] A - a;}cout << t[i];}return 0; }

windows + visual studio 2019 使用cmake 编译构建静、动态库并调用详解

环境 windows visual studio 2019 visual studio 2019创建cmake工程 1. 静态库.lib 1.1 静态库编译生成 以下是我创建的cmake工程文件结构&#xff0c;只关注高亮文件夹部分 libout 存放编译生成的.lib文件libsrc 存放编译用的源代码和头文件CMakeLists.txt 此次编译CMak…

【含文档+PPT+源码】基于微信小程序的校园志愿者管理系统的设计与实现

项目介绍 本课程演示的是一款 基于微信小程序的校园志愿者管理系统的设计与实现&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本…

SOME/IP--协议英文原文讲解5

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 这一章节…

Linux之Http协议分析以及cookie和session

Linux之Http协议分析以及cookie和session 一.分析请求行与响应行1.1请求行1.1.1资源的URL路径1.1.2常见的方法1.2响应行 二.cookie和session2.1cookie2.2session 一.分析请求行与响应行 在我们简单了解了请求和响应的格式以及模拟实现了请求和响应后我们已经可以通过网页来访问…

vue+element-ui简洁完美实现ju动漫网站

目录 一、项目介绍 二、项目截图 1.项目结构图 2.首页 3.日漫 4.更多>排行榜 5.详情页 6.简单登陆页 三、源码实现 1.路由配置 2.首页 四、总结 一、项目介绍 本项目在线预览&#xff1a;点击访问 本项目为vue项目&#xff0c;以动漫为主题来设计元素&#xff…

协议-WebRTC-HLS

是什么&#xff1f; WebRTC&#xff08;Web Real-Time Communication&#xff09; 实现 Web 浏览器和移动应用程序之间通过互联网直接进行实时通信。允许点对点音频、视频和数据共享&#xff0c;而无需任何插件或其他软件。WebRTC 广泛用于构建视频会议、语音通话、直播、在线游…

本地部署DeepSeek-R1模型(新手保姆教程)

背景 最近deepseek太火了&#xff0c;无数的媒体都在报道&#xff0c;很多人争相着想本地部署试验一下。本文就简单教学一下&#xff0c;怎么本地部署。 首先大家要知道&#xff0c;使用deepseek有三种方式&#xff1a; 1.网页端或者是手机app直接使用 2.使用代码调用API …

有关网络安全的案例分享 如何保障网络安全

网络发展是非常迅速的&#xff0c;互联网在给人们带来生活娱乐便利的同时&#xff0c;也带来了一些安全隐患&#xff0c;这就需要大家做好防骗规范&#xff0c;确保网络安全&#xff0c;51CTO学堂为大家分享下有关网络安全的案例&#xff0c;以供各位参考。 非法获取公民个人信…

2025新鲜出炉--前端面试题(一)

文章目录 1. vue3有用过吗, 和vue2之间有哪些区别2. vue-router有几种路由, 分别怎么实现3. webpack和rollup这两个什么区别, 你会怎么选择4. 你能简单介绍一下webpack项目的构建流程吗5. webpack平时有手写过loader和plugin吗6. webpack这块你平时做过哪些优化吗&#xff1f;7…

变化检测论文阅读合集

1. ChangeCLIP: Remote sensing change detection with multimodal vision-language representation learning 作者&#xff1a;Sijun Dong a, Libo Wang b, Bo Du c, Xiaoliang Meng a,* 年份&#xff1a;2024 研究方法/模型&#xff1a; 重构原始CLIP&#xff1a;提取双时…

viem库

viem是一个用于和以太坊进行交互的javascript库&#xff0c;它提供了简单的API进行智能合约的读取和写入操作&#xff0c;你可以使用它来与区块链上智能合约进行交互&#xff0c;查询链上数据等。 基本功能 1&#xff0c;创建公有客户端 createPublicClient 可以创建一个链接…