代码随想录算法训练营第二十二天(回溯 一)

news2024/11/27 14:33:13

开始学习回溯!

回溯理论基础

代码随想录文章链接:代码随想录

文章摘要:

什么是回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。

在二叉树系列中,我们已经不止一次,提到了回溯。

回溯是递归的副产品,只要有递归就会有回溯。

所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数

回溯法的效率

回溯法的性能如何呢,这里要和大家说清楚了,虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法

因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

那么既然回溯法并不高效为什么还要用它呢?

因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。

此时大家应该好奇了,都什么问题,这么牛逼,只能暴力搜索。

回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

回溯法模板

在前段时间的二叉树学习中我们说了递归三部曲,这里我再给大家列出回溯三部曲

  • 回溯函数模板返回值以及参数

回溯算法中函数返回值一般为void。

再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。

回溯函数伪代码如下:

void backtracking(参数)
  • 回溯函数终止条件

那么我们在讲解递归三部曲的时候,就知道递归函数一定要有终止条件,不能无限的递归下去。

回溯也一样,要有终止条件。

什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。

所以回溯函数终止条件伪代码如下:

if (终止条件) {
    存放结果;
    return;
}
  • 回溯搜索的遍历过程

在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

如图:

回溯算法理论基础

注意图中,我特意举例集合大小和孩子的数量是相等的!

回溯函数遍历过程伪代码如下:

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtracking(路径,选择列表); // 递归
    回溯,撤销处理结果
}

for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

backtracking这里自己调用自己,实现递归。

大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。

然后结合上面的思路,我们可以写出下面这个伪代码模板:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

看完这些,我们就开始刷题吧! 

力扣题部分:

77. 组合

题目链接:. - 力扣(LeetCode)

题面:

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

思路:

本题是回溯法的经典题目。

直接的解法当然是使用for循环,例如示例中k为2,很容易想到 用两个for循环,这样就可以输出 和示例中一样的结果,下面k = 2时的代码。

int n = 4;
for (int i = 1; i <= n; i++) {
    for (int j = i + 1; j <= n; j++) {
        cout << i << " " << j << endl;
    }
}

如果 k = 3 呢?

我们就需要三个for循环。

如果n为100,k为50呢?那就50层for循环,是不是开始窒息

此时就会发现虽然想暴力搜索,但是用for循环嵌套连暴力都写不出来!

回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像for循环嵌套k层让人绝望。

那么回溯法怎么暴力搜呢?

上面我们说了要解决 n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯法就用递归来解决嵌套层数的问题

递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了

此时递归的层数大家应该知道了,例如:n为100,k为50的情况下,就是递归50层。

如果n = 4, k = 2,回溯思路应该就是这样的: 

大致看懂这个图,接下来就让我们开始回溯三部曲

回溯函数模板返回值以及参数

代码如下:

void backtracking(int n, int k, int startIndex)

n 和 k 都没什么问题,startindex是什么呢?

startindex是保证回溯过程不会重复的一个标志。

题目的意思是{1,2}和{2,1}是同一种组合,如果我们只弄for循环没有startindex,会导致组合重复,看看下面这个for循环,如果没有startindex,i从0开始,显然会重复。

for(int i = startIndex; i <= n; i ++)

除此之外,我们还要创建两个全局变量,一个用来存放符合条件单一结果(如{1,2}),一个用来存放符合条件结果的集合。

代码如下:

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果

回溯函数终止条件

遍历过程中如果path的长度和k相等,意味着当前我们找到的组合符合条件了,这个时候我们就要把path记录下来,记录给result然后再结束本次递归。

终止条件代码如下:

if (path.size() == k) {
    result.push_back(path);
    return;
}

回溯搜索的遍历过程

其实遍历的循环外壳上面已经写过了。

for里面的内容需要注意——递归前加进来的元素需要在递归后去除。

整体代码如下:

for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
    path.push_back(i); // 处理节点
    backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
    path.pop_back(); // 回溯,撤销处理的节点
}

写到这里,代码整合一下就出来了。

我们可以结合模板看下面的代码,基本就是套了模板的壳。

代码实现:

class Solution {
public:
    vector<int>path;
    vector<vector<int>>result;
    void backtracking(int n, int k, int startIndex)
    {
        if(path.size() == k)
        {
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i <= n; i ++)
        { 
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

216.组合总和III

题目链接:. - 力扣(LeetCode)

题面:

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

思路:

上面讲了那么多,这道题就不重复了,直接回溯三部曲:

回溯函数模板返回值以及参数

和上面相比,我们需要一个通过sum来判断和是不是n来决定是否符合条件。

当然,两个全局变量是不可少的。

vector<int>rightnums;
vector<vector<int>>result;
void find(int n, int k, int sum, int index)

回溯函数终止条件

如果长度等于k,就该停止操作了,当然停止前要判断是否符合条件,符合得记得加入答案的数组。

if(rightnums.size() == k)
{
    if(sum == n) result.push_back(rightnums);
    return;
}

回溯搜索的遍历过程

这回的集合元素只有1-9,所以for循环的i从index到9就行了。遍历如图所示

处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。

代码如下:

for (int i = startIndex; i <= 9; i++) {
    sum += i;
    path.push_back(i);
    backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
    sum -= i; // 回溯
    path.pop_back(); // 回溯
}

别忘了处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!

整合一下上面的代码答案就出来了。

代码实现:

class Solution {
public:
    vector<int>rightnums;
    vector<vector<int>>result;
    void find(int n, int k, int sum, int index)
    {
        if(rightnums.size() == k)
        {
            if(sum == n) result.push_back(rightnums);
            return;
        }
        for(int i = index; i <= 9; i ++)
        {
            rightnums.push_back(i);
            sum += i;
            find(n, k, sum, i + 1);
            sum -= i;
            rightnums.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        find(n, k, 0, 1);
        return result;
    }
};

17.电话号码的字母组合

题目链接:. - 力扣(LeetCode)

题面:

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。对应关系如图所示。

思路:

回溯三部曲:

回溯函数模板返回值以及参数

vector<string>result;
    string mapnums[10] = {"6", "6","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    string ans = "";

把映射的内容记录成字符串数组放在全局变量的位置(就是下面代码的mapnums),然后还是两个全局变量,一个是放当前的组合,一个记录所有符合条件组合的答案。

回溯函数终止条件

if(ans.size() == digits.size()) 
{
    result.push_back(ans);
    return;
}

因为这个组合通过一一映射得到,显然当前组合的长度和字符串digits的长度相同时记录下来并结束当前递归函数。

回溯搜索的遍历过程

int k, i, d;
k  = ans.size();
d = digits[k] - '0';
string s;
for(i = 0; i < mapnums[d].size(); i ++)
{
    s = mapnums[d][i];
    ans += s;
    backtracking(digits);
    ans.pop_back();
}

这回的for循环要看数字对应的映射字符串数组mapnums,数字不同遍历对象也不同。

我们通过k记录当前的组合长度,那digits[k]就刚好是读取的当前数字,由于数字是字符char格式,转int需要记得减去字符0。

s代表的是读取数字可能的字符,根据mapnums和当前数字d确定映射的具体字符串,s就是字符串的每种可能性。

这样的遍历下来,每种情况都可以通过这个回溯递归函数穷举得到。

代码实现:

class Solution {
public:
    vector<string>result;
    string mapnums[10] = {"6", "6","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    string ans = "";
   
    void backtracking(string digits)
    {
        if(ans.size() == digits.size()) 
        {
            result.push_back(ans);
            return;
        }
        int k, i, d;
        k  = ans.size();
        d = digits[k] - '0';
        string s;
        for(i = 0; i < mapnums[d].size(); i ++)
        {
            s = mapnums[d][i];
            ans += s;
            backtracking(digits);
            ans.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0) return result;
        backtracking(digits);
        return result;
    }
};

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

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

相关文章

贪吃蛇(Qt版)

目录 一、项目介绍 界面一&#xff1a;游戏大厅界面 界面二&#xff1a;关卡选择界面 界面三&#xff1a;游戏界面 最终游戏效果&#xff1a; 二、项目创建与资源配置 1. 创建项目 2. 添加项目资源文件 三、项目实现 1. 游戏大厅界面 2. 关卡选择界面 3. 游戏房间界…

重装后的电脑怎么分区?轻松优化存储空间

电脑重装系统是解决许多软件问题和提升性能的有效方法。然而&#xff0c;重装系统后&#xff0c;合理的硬盘分区不仅能提高数据管理效率&#xff0c;还有助于保护系统安全。本文将详细介绍如何在重装电脑后进行合理的分区&#xff0c;帮助您更好地管理和使用您的电脑。 一、了解…

傅里叶变换与拉普拉斯变换:联系、区别及其应用

1. 傅里叶变换和拉普拉斯变换的定义 1.1 傅里叶变换的定义 傅里叶变换是将时间域信号转换为频率域信号的数学工具&#xff0c;由正向和逆变换组成。它将信号分解为正弦波和余弦波的组合&#xff0c;适用于周期性和非周期性信号分析。 1.2 拉普拉斯变换的定义 拉普拉斯变换是…

武汉流星汇聚:跨境电商领航者,以自营经验赋能万企,共绘出海蓝图

在数字经济浪潮席卷全球的今天&#xff0c;跨境电商作为国际贸易的新引擎&#xff0c;正以前所未有的速度改变着全球商业格局。在这片充满机遇的蓝海中&#xff0c;武汉流星汇聚电子商务有限公司犹如一颗璀璨的流星&#xff0c;划破长空&#xff0c;以其独特的优势和卓越的成就…

数学强化| 李林880重点题速刷计划

快9月了&#xff0c;有的同学还没开始强化&#xff0c;进度确实有点慢了&#xff0c;有同学问&#xff1a; 刚开始强化&#xff0c;880题该如何快速刷完&#xff1f; 听我说&#xff0c;别急&#xff01;越是强化开始的晚&#xff0c;就越不能急&#xff0c;因为强化的作用有两…

易基因:泪腺RRBS+RNA-seq揭示Sjögren综合征相关干眼症的潜在基因|项目文章

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 Sjgren综合征&#xff08;Sjgren’s syndrome&#xff0c;SS&#xff09;相关干眼症是一种以泪腺&#xff08;lacrimal glands&#xff0c;LGs&#xff09;慢性炎症为特征的难治性自身免…

mysql InnoDB引擎各种隔离级别的加锁机制

文章目录 概要前置知识了解各种隔离锁的验证小结 概要 我们都知道&#xff0c;mysql的InnoDB引擎在各种隔离级别下的加锁机制都是有差异的&#xff0c;但是对于各种隔离级别下如何加锁大家可能不太了解&#xff0c;今天我就通过一篇文章去带领大家去分析一下各个隔离级别的加锁…

【HTML】使用Javascript制作网页

1、Javascript的语法规则 JavaScript程序按照在HTML文件中出现的顺序逐行执行。JavaScript严格区分字母大小写。在JavaScript中&#xff0c;每行结尾的分号可有可无。JavaScript中主要包括两种注释&#xff1a;单行注释和多行注释。单行注释使用双斜线“//”作为注释标签&…

AI绘画商业实战,深入剖析Stable Diffusion 服装模特精准换装脱Y,AI虚拟模特变现教程

大家好&#xff0c;我是灵魂画师向阳 在之前的文章中&#xff0c;我们已经深入讲解了SD与ControlNet基础知识和原理。接下来我们将结合这一堆基础工具法宝组合使用&#xff0c;完成一些有意义的AIGC商业实战案例分享。 本文是来自一位粉丝的现实需求案例&#xff1a;电商服装…

人在上海ip显示在安徽怎么回事?怎么办

在这个信息爆炸的时代&#xff0c;网络已成为我们生活中不可或缺的一部分。无论是工作、学习还是娱乐&#xff0c;网络都以其独特的魅力渗透进我们生活的每一个角落。然而&#xff0c;随着网络技术的不断发展&#xff0c;一些看似不可思议的现象也逐渐浮出水面。比如&#xff0…

海思SD3403/SS928V100开发(16)Tsensor驱动开发

1. 前言 由于需要检测SD3403芯片内部实时温度,需要开发Tsensor传感器驱动和应用 查看手册发现SD3403内部有三个Tsensor传感器 可以参考之前我写的35系列平台Tsensor驱动开发记录 海思35系列平台Tsensor驱动开发(1)驱动编写_t sensor-CSDN博客 海思35系列平台Tsensor驱动…

什么是埋点测试,app埋点测试怎么做?

前言 埋点测试是指在应用程序或网站中预设检查点&#xff0c;收集程序运行时的数据&#xff0c;以便于后续对程序进行性能分析或故障排查。埋点测试通常用于监控和追踪用户在软件产品中的行为&#xff0c;以收集有关用户体验、功能使用情况和潜在问题的数据。这些数据对于软件…

哪些ai取名网站免费?盘点4大好用的ai取名字自动生成器

在忙碌而喧嚣的都市生活中&#xff0c;越来越多人选择养宠物作为自己的精神寄托。当你决定迎接一只新生命回家时&#xff0c;除了准备好食物等必需品外&#xff0c;更重要的是给它起一个既好听又有意义的名字。 然而&#xff0c;有时候想出一个合适的名字并不容易&#xff0c;…

中电金信:稳定运行超百天!业内首个100%全栈国产化手机银行上线

日前&#xff0c;国内首个全栈国产化手机银行-华润银行新一代手机银行已经正式投产超百天&#xff0c;系统运行稳定&#xff0c;性能显著提升&#xff0c;客户体验明显改善&#xff0c;达成了“新理念、新体验、新技术、新底座”的建设目标&#xff0c;整体水平处于行业中上游。…

对射式光电开关应用

图 5-38a所示是自动启停扶梯的示意图。扶梯入口安装一个对射式光电开关&#xff0c;当有人走到扶梯口要上扶梯时&#xff0c;挡住了光电开关发光部分所发出的光&#xff0c;使得受光部分无法接收到光信号&#xff0c;由此控制扶梯启动&#xff0c;延时一段时间后&#xff0c;扶…

ubuntu上cmake3.30.2的安装

引言 安装下载安装包将安装包从windows拷贝到ubuntu解压进入解压后的文件夹执行boostrap编译CMake安装CMake查看是否安装成功 目前的ubuntu系统是20.04.4&#xff0c;用命令行安装了cmake的版本是3.16的&#xff0c;由于项目需要升级cmake到cmake3.22之上&#xff0c;使用命令行…

揭秘led台灯对眼睛好不好?护眼台灯真的护眼吗?台灯要这样选!

在当今数字化时代&#xff0c;长时间面对电脑屏幕和各种电子设备已成为日常生活的一部分&#xff0c;这对我们的视力构成了前所未见的挑战。目前中国的近视情况十分严峻&#xff0c;尤其在青少年群体中表现得更为突出。因此&#xff0c;科学用眼、合理安排用眼时间和增加户外活…

新手起步:探索AWS新账户的服务器部署能力与限制

对于刚刚注册Amazon Web Services (AWS)账户的用户来说&#xff0c;一个常见的疑问是&#xff1a;我能立即开始部署服务器吗&#xff1f;这个问题的答案是肯定的&#xff0c;但同时也需要注意一些重要的细节。我们九河云将深入探讨新注册的AWS账户在服务器部署方面的能力和注意…

AI大模型应用开发实战-Agent应用对话情感优化

1 使用prompt设计agent性格与行为 添加系统 prompt&#xff1a; 代码语言&#xff1a;python 代码运行次数&#xff1a;0 复制 Cloud Studio 代码运行 self.SYSTEMPL """你是一个非常厉害的算命先生&#xff0c;你叫JavaEdge人称Edge大师。以下是你的个人…

数据库MySQL多表设计、查询

目录 1.概述 2.一对多 3.一对一 4.多对多 5.多表查询 5.1内连接 5.2外连接 5.3子查询 1.概述 项目开发中,在进行数据库表结构设计时&#xff0c;会根据业务需求及业务模块之间的关系&#xff0c;分析并设计表结构&#xff0c;由于业务之间相互关联&#xff0c;所以各个…