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

devtools/2024/10/19 9:43:29/

目录

一、做题心得

二、回溯基础知识

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.ppmy.cn/devtools/90890.html

相关文章

Android 14 Power键亮灭屏流程

Android14中Power键的事件分发和Android10的是不一样的&#xff0c;这里并没有经过interceptKeyBeforeDispatching方法&#xff0c;而是直接走到了interceptKeyBeforeQueueing方法 PhoneWindowManager中的堆栈如下 07-06 08:59:04.481 1844 1984 D WindowManager: intercep…

ETL工程师角度下的SQL优化

作为ETL&#xff08;Extract, Transform, Load&#xff09;工程师&#xff0c;SQL优化是提高数据处理和分析效率的关键一环。优化SQL查询可以显著降低数据处理时间&#xff0c;提高ETL过程的性能。本文将从 合理设计数据模型&#xff1a;在ETL过程中&#xff0c;正确的数据模型…

LeetCode 0572.另一棵树的子树:深搜+广搜(n^2做法就能过,也有复杂度耕地的算法)

【LetMeFly】572.另一棵树的子树&#xff1a;深搜广搜&#xff08;n^2做法就能过&#xff0c;也有复杂度耕地的算法&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/subtree-of-another-tree/ 给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 s…

Linux修炼之路之进程地址空间

目录 一&#xff1a;程序地址空间 二&#xff1a;相关细节知识 接下来的日子会顺顺利利&#xff0c;万事胜意&#xff0c;生活明朗-----------林辞忧 一&#xff1a;程序地址空间 1.在学习c/c时&#xff0c;经常会听到堆区&#xff0c;栈区&#xff0c;代码段&#xff0c;常量…

6种创造型设计模式

创造型设计模式 工厂模式简单工厂模式工厂方法模式抽象工厂模式 单例模式懒汉模式饿汉模式静态内部类 原型模式建造者模式PS 工厂模式 工厂模式是为了更好管理new出来的对象, 把创建对象的任务交给工厂做, 比手动new更符合软件设计原则 简单工厂模式 包括三个角色 工厂角色…

Matplotlib | 绘制折线图

目录 简介安装 Matplotlib开始绘制简单折线图改变线的样式改变节点的样式添加图表文字改变坐标轴标签改变坐标数值范围绘制多条折线实践&#xff1a;绘制温度变化图 简介 折线图&#xff08;Line Chart&#xff09;&#xff0c;是一种以折线来呈现数据随时间变化而变化的图表。…

数据结构——排序(1):插入排序

目录 一、排序的概念 二、排列的运用 三、常见的排序算法 四、插入排序 1.直接插入排序 &#xff08;1&#xff09;思路 &#xff08;2&#xff09;过程图示 &#xff08;3&#xff09;代码实现 (4)代码解释 &#xff08;5&#xff09;特性 2.希尔排序 &#xff08;1…

获取客户端真实IP

出于安全考虑&#xff0c;近期在处理一个记录用户真实IP的需求。本来以为很简单&#xff0c;后来发现没有本来以为的简单。这里主要备忘下&#xff0c;如果服务器处于端口回流&#xff08;hairpin NAT&#xff09;,keepalived&#xff0c;nginx之后&#xff0c;如何取得客户端的…