【代码随想录训练营第42期 Day22打卡 回溯Part1 - LeetCode 77. 组合 216.组合总和III 17.电话号码的字母组合

目录

一、做题心得

二、回溯基础知识

1.定义

2.适用问题

3.一个思想

4.代码实现

三、题目与题解

题目一:77. 组合 

题目链接

题解:回溯

 题目二:216.组合总和III

题目链接

题解:回溯

 题目三:17.电话号码的字母组合 

题目链接

 题解:回溯

四、小结

一、做题心得

今天是代码随想录打卡的第22天,也是成功来到了回溯章节。回溯的话,其实之前二叉树递归思路讲解的时候也有提到,回溯基本就是基于递归之下的一个实现。今天的题应该算是很经典的回溯的应用了:组合问题。作为回溯的入门,今天也是通过做题理解到了很多相关的知识,还有就是,模板的使用。

话不多说,直接开始今天的内容。

二、回溯基础知识

1.定义

回溯算法(Backtracking Algorithm)是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解,即“回溯”到上一步,并尝试另一个候选解。这个过程一直进行,直到找到所有解或确定无解为止。

2.适用问题

回溯算法常用来解决那些可以分解为多个步骤或子问题的复杂问题,特别是在这些子问题之间存在相互依赖关系,且每个子问题的解空间可以明确地枚举出来时。它特别适用于寻找问题的所有解,而不是单一最优解的情况。

比如:

  1. 排列组合问题:如全排列、组合、子集、幂集等问题,可以通过递归地尝试每个可能的元素来构建解。

  2. 分割问题:如将集合划分为满足特定条件的子集,这类问题可以通过回溯来尝试不同的划分方式。

  3. 棋盘问题:如八皇后问题、N皇后问题、骑士巡逻(骑士在棋盘上遍历每个格子恰好一次)等,这类问题涉及在二维空间上放置或移动对象以满足特定条件。

  4. 图的着色问题:给定一个图,要求用最少的颜色给图中的每个顶点着色,使得任意两个相邻的顶点颜色不同。

  5. 布尔满足性问题(SAT):确定是否存在一组布尔变量的赋值,使得给定的布尔表达式为真。

  6. 子集和问题:从给定的整数数组中找出所有可能的子集,使得子集中的元素之和等于一个特定的目标值。

  7. 路径寻找问题:如迷宫问题、旅行商问题(TSP)的近似解可以通过回溯法得到(虽然TSP的精确解通常使用动态规划或其他更高效的算法)。

  8. 字符串处理问题:如字符串的排列、生成所有可能的括号组合等。

  9. 决策树/游戏树的遍历:在决策制定过程中,如棋类游戏或任何需要评估多个可能选择并作出最佳决策的场景中,回溯算法可以用来遍历所有可能的决策路径。

回溯法的本质还是枚举,效率并不高,但是为了解决以上这些问题,回溯依旧是最优的选择。

3.一个思想

回溯的搜索遍历过程类似于对树的搜索遍历

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

树的宽度我们可以通过横向遍历实现:for循环

树的深度我们可以通过纵向遍历实现:递归

4.代码实现

回溯算法的实现往往采取同一套适用的模板,每个人的做题习惯不同,可能会存在一些差异,但是终归做题的思路是无异的。这里给出一套代码随想录回溯部分的模板:

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

以上模板的具体含义以及如何通过这个模板实现解决各种问题,我将会在后边几道题中提到。

三、题目与题解

题目一:77. 组合 

题目链接

77. 组合 - 力扣(LeetCode)

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

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

示例 1:

输入:n = 4, k = 2
输出:
[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n
题解:回溯

作为回溯章节的第一道题,也是解决组合问题中最经典的一道题,我们可以通过这道题初步感受一下回溯是如何实现的。

  1. 定义全局变量
    • ans:一个二维向量,用于存储所有生成的组合结果。每个内部向量代表一个组合。
    • vec:一个一维向量,用于在回溯过程中临时存储当前正在构建的组合。
  2. 回溯函数:backtrack(int n, int k, int start)
    • 参数 n 表示可选数字的上限(即从1到n中选择)。
    • 参数 k 表示每个组合中应包含的元素数量。
    • 参数 start 表示当前搜索的起始位置,用于避免生成重复的组合并优化搜索空间。
  3. 终止条件
    • 当vec的大小等于k时,说明已经找到了一个符合条件的组合,将其加入到ans中,并返回上一层递归(即return)。
  4. 递归搜索与剪枝
    • 使用一个循环从 start 开始遍历到 n - (k -  vec.size()) + 1。这里的剪枝操作 n - (k -  vec.size()) + 1 是为了提前结束不必要的搜索,因为如果剩余可选的数字不足以构成长度为 k 的组合,那么就没有必要继续搜索。
    • 在循环内部,将当前数字 i 添加到 vec 中,然后递归调用 backtrace 函数,并将搜索的起始位置设为 i + 1(避免重复使用同一个数字)。
    • 递归返回后,通过vec.pop_back()撤销vec中的最后一个元素,实现回溯,以便尝试其他可能的组合。
  5. 主函数:combine(int n, int k)
    • 初始化ans和vec,并调用backtrace函数从数字1开始构建组合。
    • 返回所有生成的组合结果ans

我对代码也进行了详细注释,代码如下:

class Solution {
public:vector<vector<int>> ans;            //用于存放所有生成的组合结果vector<int> vec;                    //用于存放当前正在构建的组合void backtrack(int n, int k, int start) {           //回溯函数,用于递归地生成组合if (vec.size() == k) {              //终止条件:组合的大小等于kans.push_back(vec);return;                  //找到了一个有效组合,返回上一级递归}for (int i = start; i <= n - (k - vec.size()) + 1; i++) {          //从start开始遍历,i表示某次搜索的起始位置(注意剪枝操作的优化)vec.push_back(i);               //处理节点backtrack(n, k, i + 1);        //递归:控制树的纵向遍历,注意下一层搜索要从i+1开始vec.pop_back();             //回溯,撤销处理的节点}}vector<vector<int>> combine(int n, int k) {backtrack(n, k, 1);         //调用回溯函数从数字1开始构建组合  return ans;}
};

当然,剪枝操作主要是为了降低复杂度,如果实在想不到剪枝,也可以直接 for 循环从 i = start 到 i = n。

 题目二:216.组合总和III

题目链接

216. 组合总和 III - 力扣(LeetCode)

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

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

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

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

提示:

  • 2 <= k <= 9
  • 1 <= n <= 60
题解:回溯

 这个题有了上一道题的基础其实就比较简单了,套我们提到的代码实现的模板。个人感觉唯一和上一道题不同的就是终止条件的地方了--即 if 判断语句那里。

这里先看看我自己写的代码(个人感觉更好理解,毕竟就是上一道题的变形,模板的套用):

 并没有想到具体的剪枝的操作,只是套用模板照猫画虎给整出来了,还有击败100%,有点意外。

这里我们看看代码随想录运用到了剪枝的代码:

class Solution {
public:vector<vector<int>> result; // 存放结果集vector<int> path; // 符合条件的结果void backtracking(int targetSum, int k, int sum, int startIndex) {if (sum > targetSum)        return;         //剪枝操作if (path.size() == k) {if (sum == targetSum)       result.push_back(path);return;             // 如果path.size() == k 但sum != targetSum 直接返回}for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝sum += i;               // 处理path.push_back(i);           // 处理backtracking(targetSum, k, sum, i + 1);         // 注意i+1调整startIndexsum -= i;                   // 回溯path.pop_back();             // 回溯}}vector<vector<int>> combinationSum3(int k, int n) {backtracking(n, k, 0, 1);return result;}
};

 题目三:17.电话号码的字母组合 

题目链接

17. 电话号码的字母组合 - 力扣(LeetCode)

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

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

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。
 题解:回溯

这道题的话,看上去就要难不少了。

其实题意挺简单,就是将给定的字符串中的数字全部转为它们可以表示的字母替换:这里需要注意的是,0和1并没有对应的字母,而其他数字分别对应着3个字母即对应着三种情况。

这其实也是组合问题的一种变形,我们首先就是要想到使用回溯。回溯的话,我们套用上述的模板,想清楚终止条件,横向遍历,纵向遍历等等。当然这道题还有一个关键点就是哈希表(哈希映射)的使用,我们要通过哈希表将每个数字对应的字母(由于一个数字对应多个字母,就会是字符串型)存储下来,以便于后续的转换。

class Solution {
public:vector<string> ans;         //用于存储所有可能的字母组合(结果)string tmp;               //用于构建存储当前字母组合vector<string> hash = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};      //哈希(映射)表,将数字映射到对应的字母字符串,注意0和1无效,对应的字母为空void backtrace(int pos, string digits) {        //回溯--用于递归地生成所有可能的字母组合 if (pos == digits.size()) {             //终止条件:字符串中所有数字都已经处理完毕ans.push_back(tmp);return;}int num = digits[pos] - '0';    //取出当前位置(pos:从0开始)的数字(但以字符的形式),并转换为对应的索引numfor (int i = 0; i < hash[num].size(); i++) {        //for循环:横向遍历字符串中每个字符tmp.push_back(hash[num][i]);        //处理当前位置backtrace(pos + 1, digits);     //递归:纵向遍历--注意从下个位置pos + 1开始tmp.pop_back();}}vector<string> letterCombinations(string digits) {if (digits.size() == 0)     return ans;         //给定字符串为空backtrace(0, digits);return ans;}
};

四、小结

今天的打卡到此也就结束了,后续回继续进行回溯的相关练习。最后,我是算法小白,但也希望终有所获。

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

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

相关文章

第十九天(2024.8.7)Vue Element-plus

1.Vue 1.创建vue文件 1.创建一个文件夹来存储vue文件 我在D盘下创建了一个EasyVue文件夹来存储vue文件 2.在控制台中输入 如果在控制台中按下面步骤成功不了的话&#xff0c;尝试&#xff1a;1.用管理员身份运行控制台 2.关闭防火墙 3.打开编码工具&#xff08;Visual St…

WPF学习(5)- Border控件(边框布局)+GridSplitter分割窗口

严格来说&#xff0c;Border并不是一个布局控件&#xff0c;因为它并不是Panel的子类&#xff0c;而是Decorator装饰器的子类&#xff0c;而Decorator继承于FrameworkElement。我们要先看看它的父类Decorator。 public class Decorator : FrameworkElement, IAddChild {public…

CUDA编程05 - GPU内存架构和数据局部性

一&#xff1a;概述 到目前为止&#xff0c;我们已经学会了如何编写 CUDA 核函数&#xff0c;以及如何设置和分配大量线程来执行核函数。我们还了解了当前 GPU 硬件的计算架构&#xff0c;以及线程在硬件上调度执行过程。在本章中&#xff0c;我们将重点关注 GPU 的片上(on-chi…

Redisson 实现分布式锁

文章目录 Redisson 是什么Redisson 使用客户端模式单节点模式哨兵模式主从模式集群模式Spring Boot 整合 Redisson 中的锁Redisson 可重入锁Redisson 公平锁Redisson 联锁Redisson 读写锁Redisson Redlock Redisson 的看门狗机制RedLock 解决单体故障问题如何使用 RedLockMarti…

【C语言篇】操作符详解(上篇)

文章目录 操作符详解&#xff08;上篇&#xff09;前言sizeof强制类型转换算术操作符赋值操作符逻辑操作符逻辑取反运算符逻辑与运算符逻辑或运算符 关系操作符自增自减操作符和-逗号表达式 操作符详解&#xff08;上篇&#xff09; 前言 操作符又被叫做运算符&#xff0c;是不…

进程状态(三)----- linux 中具体的进程状态(下)

目录 前言1. T && t 状态2. X 与 Z 状态3. 孤儿进程 前言 继上一篇文章 进程状态&#xff08;二&#xff09;----- linux 中具体的进程状态&#xff08;上&#xff09; 介绍了 linux 系统中具体的 R、S、D 状态&#xff0c;而这篇文章继续介绍 linux 系统中剩下的三种…

SpringBoot简单项目(二维码扫描)

pom.xml中导入依赖 <!-- zxing --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.0</version></dependency><dependency><groupId>com.google.zxing</gro…

探索七款前沿UI设计软件:创新与实践

之前我们分享了制作原型的有用工具。制作完原型后&#xff0c;我们需要优化界面&#xff0c;这就是 UI 设计师的任务了。UI 设计软件对设计师来说非常重要。UI 设计工具的使用是否直接影响到最终结果的质量&#xff0c;所以有人会问&#xff1a;UI 界面设计使用什么软件&#x…

Java批量查询CSDN质量分

文章目录 前言代码实现pom.xml实体类工具类质量分查询 效果开源仓库 前言 在CSDN平台申请“专家博主”、“优质创作者”等称号的时候&#xff0c;往往会对博客的质量分有一定的要求&#xff0c;这时候我们需要审视以往所发表的博客&#xff0c;找出质量分较低的博客&#xff0…

nordic 蓝牙ble 配对绑定的流程 原理

目录 配对和绑定的基本概念 配对和绑定的流程 1. 配对请求和响应 2. 配对方法选择 3. 密钥生成和交换 4. 配对完成和绑定 配对和绑定的代码实现 初始化Peer Manager 处理Peer Manager事件 处理BLE事件 启动广播 在Nordic芯片上实现蓝牙低功耗(BLE)设备的配对和绑定…

Python 为Excel单元格设置填充\背景色 (纯色、渐变、图案)

在使用Excel进行数据处理和分析时&#xff0c;对特定单元格进行背景颜色填充不仅能够提升工作表的视觉吸引力&#xff0c;还能帮助用户快速识别和区分不同类别的数据&#xff0c;增强数据的可读性和理解性。 本文将通过以下三个示例详细介绍如何使用Python在Excel中设置不同的单…

sql注入——sqlilabs1-15

目录 sql注入靶场练习--sqlilabs 1.less-1​编辑 1.测试发现单引号为逃逸符号 2.确定查询列数为三列 3.查询到数据库名 4.查询数据库中的表名 5.查询用户表的列名字 6.查询用户信息 2.less-2​编辑 2.确定查询列数为三列 3.查询到数据库名 4.查询数据库中的表名 5.…

机械学习—零基础学习日志(高数23——无穷小运算)

零基础为了学人工智能&#xff0c;真的开始复习高数 这段时间&#xff0c;把张宇老师讲解考研的第一部分基本全部学习完毕了。 这里把第一部分的内容最后汇总一下。 无穷小运算——吸收律 这里展示一些无穷小的具体计算思路 无穷小运算——计算方法 泰勒展开的原则 夹逼准则…

SQL报错注入之floor

目录 1.简述 2.关键函数说明 1.rand函数 2.floor&#xff08;rand&#xff08;0&#xff09;*2&#xff09;函数 3.group by 函数 4.count&#xff08;*&#xff09;函数 3.报错分析 4.报错流程 4.1寻找注入点 4.2爆数据库名 4.3爆表名 4.4爆字段名 4.5查询数据 1.…

PySide入门实战之五 | 信号与槽函数之鼠标、键盘等事件

&#x1f680;&#x1f680;&#x1f680; Pyside6实战教程专栏目录入口&#xff1a;点击跳转 目录 一、前期准备二、鼠标触发事件鼠标拖动窗口 一、前期准备 我们采用Pyside入门实战之四中通过QTDesigner创建的界面&#xff0c;具体由两个Label和一个Button组件构成&#xff…

【c++】基础知识——快速入门c++

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C 目录 前言 一、手搓一个Hello World 二、命名空间namespace 1.命名空间的定义 2.命名空间的使用 3.命名空间补充知识 三、c中的输入和输出 四、缺省参…

图书馆座位再利用小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;座位信息管理&#xff0c;座位预订管理&#xff0c;互勉信息管理&#xff0c;意见反馈管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;我的 开发…

Unity补完计划 之Tilemap

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 1.Tilemap 是什么 Q&#xff1a;和 SpriteShape有什么区别&#xff1f; A&#xff1a;tilemap强项在于做重的复背景&…

VsCode无法远程调试

一、问题描述 按照《VsCode gdb gdbserver远程调试C程序》中介绍的方法&#xff0c;配置好VsCode后&#xff0c;按下F5快捷键&#xff0c;或点击“Start Debugging”按钮&#xff0c;没有反应&#xff0c;无法启动调试&#xff1a; 二、解决方法 针对该问题&#xff0c;我尝…

常用设计模式总结

代码的评判角度 常见的评判代码好坏的词汇&#xff1a; 灵活性&#xff08;flexibility&#xff09;、可扩展性&#xff08;extensibility&#xff09;、可维护性&#xff08;maintainability&#xff09;、可 读性&#xff08;readability&#xff09;、可理解性&#xff08;…