DAY27||回溯算法基础 | 77.组合| 216.组合总和Ⅲ | 17.电话号码的字母组合
回溯算法基础知识
一种效率不高的暴力搜索法。本质是穷举。有些问题能穷举出来就不错了。
回溯算法解决的问题有:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
如何理解
回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
- 回溯搜索的遍历过程
在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
如图:
回溯算法模板
void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯,撤销处理结果}
}
77.组合
题目:77. 组合 - 力扣(LeetCode)
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
递归里有for循环。
抽象成树形结构
然后就是经典的回溯三部曲
·递归函数的参数和返回值
·回溯终止条件
·单层递归逻辑(单层搜索过程)
代码
class Solution {vector<vector<int>>result;vector<int>path;private:void backtraking(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);backtraking(n,k,i+1);//递归搜索path.pop_back();//回溯,撤销操作}}
public:vector<vector<int>> combine(int n, int k) {backtraking(n,k,1);return result;}
};
这个和找二叉树路径总和那题还挺像的。
剪枝优化写法
来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。
这么说有点抽象,如图所示:
图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。
所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
优化过程:
已经选择的元素个数:path.size();
还需要的元素个数为: k - path.size();
在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
所以优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
216.组合总和Ⅲ
题目:216. 组合总和 III - 力扣(LeetCode)
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
图思路
未剪枝版本
class Solution {private:vector<vector<int>>result;vector<int>path;void backtraking(int k,int n,int sum,int startindex){if(path.size()==k){if(sum==n)result.push_back(path);return;//如果path.size() == k 但sum != targetSum 直接返回}for(int i=startindex;i<=9;i++){sum+=i;path.push_back(i);backtraking(k,n,sum,i+1);//回溯操作,发生于当第一个if条件满足后,不管sum是否得到结果,然后继续遍历所有情况sum-=i;path.pop_back();}//这里注意我自己写的差别就是,不需要if判断sum是否大等小于目标值的情况。}
public:vector<vector<int>> combinationSum3(int k, int n) {result.clear();path.clear();backtraking(k,n,0,1);return result;}
};
在回溯算法中,回溯操作的发生时机通常是在满足某种条件后或者在尝试所有可能性后。具体来说,在组合求和问题中,回溯操作的发生时机有两个主要情况:
条件达成时的回溯:
当当前路径 path 的长度达到了要求的 k(即 path.size() == k),并且当前路径的和 sum 满足了要求(例如 sum == n),此时会将当前路径 path 加入到结果集 result 中,并进行回溯操作。
回溯操作包括将当前操作的影响从当前路径和当前状态中撤销,以便尝试其他可能的路径组合。
递归调用结束后的回溯:在递归调用中,当完成了对当前数字的处理后(通常是尝试加入当前数字并递归处理下一个数字),会进行回溯操作。(!!!!这里很重要,在打日志时就可以看到回溯是如何进行的了)
这种情况下,回溯操作是为了确保在递归返回到当前层级时,状态已经被恢复到递归前的状态,从而可以尝试其他可能的数字组合。
在具体的实现中,回溯操作通常包括以下步骤:····加入当前选择:将当前数字加入到路径 path 中,并更新当前的和 sum。
····递归处理:递归调用下一层级处理下一个数字。
····回溯:在递归调用返回后,撤销当前数字的选择,将其从路径 path 中移除,并恢复当前的和 sum 到递归前的状态。
回溯操作的发生时机是保证算法能够在所有可能的路径中搜索,并且在满足条件或者确定无法满足条件时及时回溯,以尝试其他可能的组合。
以本题为例子理解的回溯过程。
acm模式代码如图
#include <iostream>
#include <vector>using namespace std;vector<vector<int>> result;
vector<int> path;void backtracking(int k, int n, int sum, int startindex) {if (sum > n)return;if (path.size() == k) {if (sum == n) {result.push_back(path);cout << "存入一条路径" << endl;}return;}for (int i = startindex; i <= 9; i++) {sum += i;path.push_back(i);cout << "路径加入一个数" << endl;backtracking(k, n, sum, i + 1);// 回溯操作sum -= i;path.pop_back();cout << "撤销操作,回溯到上一个状态,寻找其他数字组合" << endl;}
}vector<vector<int>> combinationSum3(int k, int n) {result.clear();path.clear();cout << "调用combinationSum3函数" << endl;backtracking(k, n, 0, 1);return result;
}int main() {int k, n;// 从标准输入读取数据while (cin >> k >> n) {result = combinationSum3(k, n);// 输出结果for (const auto& combination : result) {for (int num : combination) {cout << num << " ";}cout << endl;}}return 0;
}
日志如图:
可以看出,找到第一条路径后,就开始剪枝操作了。
从这里开始,有两条一模一样的撤销操作和加入数的操作。证明一个数字和它的所有组合已经遍历完了。将从第二个数字开始查找符合条件的组合。
如果有第二条路径的话
路径加入一个数
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
路径加入一个数
开始剪枝
撤销操作,回溯到上一个状态,寻找其他数字组合
1 3
剪枝情况
在递归函数开头剪枝(好
if (sum > targetSum) { // 剪枝操作return;
}
17.电话号码的字母组合
题目:17. 电话号码的字母组合 - 力扣(LeetCode)
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
- 输入:"23"
- 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序
思路
回溯的树形结构理解
深度是用户按键的digit长度,如例子中就是2.递归函数有一个参数index。。这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。
而横向遍历的宽度,即for循环次数获取到的所有结果集。
代码
class Solution {private://键盘按键数字和字母的映射const string letterMap[10] = {"", // 0"", // 1"abc", // 2"def", // 3"ghi", // 4"jkl", // 5"mno", // 6"pqrs", // 7"tuv", // 8"wxyz", // 9};
public:vector<string>result;string s;void backtraking(const string& digits,int index){if(index==digits.size())//当 index 达到 digits.size() 时,说明已经遍历完所有的数字,将当前的组合 s 添加到 result 中,然后返回。{result.push_back(s);return;}int digit=digits[index]-'0';//string类型转intstring letters=letterMap[digit];for(int i=0;i<letters.size();i++){s.push_back(letters[i]);backtraking(digits,index+1);//探索下一层数字对应的字母了s.pop_back();//回溯,在递归返回后,通过 s.pop_back() 撤销之前的选择,尝试其他字母组合。}}vector<string> letterCombinations(string digits) {result.clear();if(digits.size()==0)return result;backtraking(digits,0);return result;}
};
我的疑惑:index不会递增下去吗?
回溯的执行过程和
index
的变化以
digits = "23"
为例,递归处理时的index
变化如下:
初始调用
backtracking("23", 0)
:
index = 0
,处理digits[0] = '2'
,对应的字母是"abc"
。- 第一个递归调用:选择
'a'
,调用backtracking("23", 1)
。递归调用
backtracking("23", 1)
:
index = 1
,处理digits[1] = '3'
,对应的字母是"def"
。- 第一个递归调用:选择
'd'
,调用backtracking("23", 2)
。递归调用
backtracking("23", 2)
:
- 此时
index = 2
,已经达到了digits.size()
,所以将当前组合"ad"
加入结果result
中,并返回。- 这里
index
不再继续增加,而是返回到上一层(backtracking("23", 1)
)。回溯到
backtracking("23", 1)
:
- 从组合
"ad"
中撤销'd'
,并选择下一个字母'e'
,继续递归调用backtracking("23", 2)
。递归调用
backtracking("23", 2)
:
- 再次达到
index = 2
,组合"ae"
加入结果,并返回到上一层。继续回溯:撤销
'e'
,选择'f'
,重复上述过程,将"af"
加入结果。完全处理完
'a'
后,回到backtracking("23", 0)
:
- 撤销
'a'
,选择'b'
,重复上述过程,处理"bd"
,"be"
,"bf"
。最后处理
'c'
,得到"cd"
,"ce"
,"cf"
。为什么
index
不会一直递增?
- 每次递归调用时,
index
加1
,是为了处理下一个数字对应的字母组合。- 当递归返回时,
index
会回到上一层,并且回到上一层的状态后,会尝试其他的可能性(选择下一个字母),然后再进行递归处理。- 每次递归到达终点(即
index == digits.size()
)时,递归结束,将当前的路径结果存入result
,接着回溯撤销选择,并返回上一层。
一些写法,是把回溯的过程放在递归函数里了
如for循环变成了这样,相应的,递归参数多了个string s,初始值是“”
for (int i = 0; i < letters.size(); i++) {getCombinations(digits, index + 1, s + letters[i]);
这里隐藏了回溯的写法。
要彻底理解回溯的过程,可以自己打下日志,逐行看下代码进行过程。
相关文章:

DAY27||回溯算法基础 | 77.组合| 216.组合总和Ⅲ | 17.电话号码的字母组合
回溯算法基础知识 一种效率不高的暴力搜索法。本质是穷举。有些问题能穷举出来就不错了。 回溯算法解决的问题有: 组合问题:N个数里面按一定规则找出k个数的集合切割问题:一个字符串按一定规则有几种切割方式子集问题:一个N个数…...

js基础速成12-正则表达式
正则表达式 正则表达式(Regular Expression)或 RegExp 是一种小型编程语言,有助于在数据中查找模式。RegExp 可以用来检查某种模式是否存在于不同的数据类型中。在 JavaScript 中使用 RegExp,可以使用 RegExp 构造函数࿰…...

使用Selenium自动化测试定位iframe以及修改img标签的display属性值
在使用 Selenium 进行自动化测试时,处理 iframe 是一个常见问题。当页面中出现 iframe 时,需要先切换到该 iframe 内部,才能正常定位和操作其中的元素。以下是处理 iframe 的步骤和示例代码: 步骤 切换到 iframe:使用…...

DAY13
面试遇到的新知识点 char str[10],只有10个字符的空间,但是只能存储9个字符,最后一个字符用来存储终止符\0 strlen只会计算\n,不会计算\0 值传递: void test2(char * str) {str "hello\n"; }int main() {char * str;test2(str);…...

WPF 自定义用户控件(Content根据加减按钮改变值)
前端代码: <UserControl.Resources><Style x:Key"Num_Button_Style" TargetType"Button"><Setter Property"MinWidth" Value"30" /><Setter Property"Height" Value"35" />&l…...

CPU、GPU、显卡
CPU VS GPUCPU(Central Processing Unit),中央处理器GPU(Graphics Processing Unit),图形处理单元GPU 的技术演变CUDA(Compute Unified Device Architecture) 显卡(Video…...

深入理解 Django 自定义用户模型
1. 引言 Django 作为一个强大的 Web 框架,内置了用户认证系统。然而,实际项目中我们通常需要扩展用户模型,以满足不同的业务需求。Django 提供了继承 AbstractUser 的方式,让我们能够轻松地定制用户模型。本文将通过一个自定义用…...

顺序表和链表的区别
顺序表和链表的区别 不同点顺序表链表(带头双向循环)存储空间物理上一定连续逻辑上连续物理上不一定连续随机访问(用下标随机访问)支持:O(1)不支持:O(N)任意位置插入或者删除元素可能需要搬移元素…...

系分-数据库总结
历年试题2024年05月试题 BCN范式,模式分解,触发器类型2023年05月试题 NoSQL基本特点,NoSQL对比,混合数据库2022年05月试题4 两段锁,事务并发,数据一致,本地事务发布20…...

new Date()解析
JavaScript 中的 new Date() 构造函数用于创建一个表示日期和时间的对象。Date 对象使得你可以以多种方式获取、设置和格式化日期和时间。让我们深入解析一下 new Date() 及其用法。 创建 Date 对象 可以通过多种方式创建 Date 对象: 不带参数: let no…...

df 的各种用法 以及与du 的区别
df的用法 在 Linux 中,“df”(disk free)是一个用于显示磁盘空间使用情况的命令。 一、主要功能 它可以列出文件系统的磁盘空间使用情况,包括磁盘总容量、已使用空间、可用空间以及使用率等信息。 二、常见用法及参数 基本用法&a…...

2024年下半年软考准考证什么时候打印?
2024年下半年软考准考证打印入口网址如下: https://bm.ruankao.org.cn/sign/welcome 广东的同学特别注意:准考证打印截止时间是11月8号,也就是考试前一天。一定要提前打印准考证,考试当天是无法打印的。 2024年下半年软考准考证…...

企业安全运行与维护(Enterprise Security Operation and Maintenance)
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…...

每日“亿“题 东方博宜OJ 1424-自然数的分解
原题链接:1424 - 自然数的分解-东方博宜OJ 题目描述 给定自然数 n ,将其拆分成若干自然数的和。输出所有解,每组解中数字按从小到大排列。相同数字的不同排列算一组解。 如,读入整数 3 ,分解方案如下: …...

初识Linux · 文件(1)
目录 前言: 回顾语言层面的文件 理解文件的预备知识 文件和磁盘 使用和认识系统调用函数 前言: 本文以及下篇文章,揭露的都是Linux中文件的奥秘,对于文件来说,初学Linux第一节课接触的就是文件,对于C…...

【MYSQL】mysql约束---自增长约束(auto_increment)
1、概念 在Mysql中,当主键为自增长后,这个主键的值就不再需要用户输入数据了,而由数据库系统根据定义自动赋值。每增加一条记录,主键会自动以相同的步长进行增长。 注意:自增长约束通常与主键放在一起使用。 通过给…...

基于STM32设计的智能学习台灯(华为云IOT)(238)
文章目录 一、前言1.1 项目介绍【1】开发背景【2】项目实现的功能【3】项目硬件模块组成【4】ESP8266工作模式配置1.2 设计思路【1】整体设计思路【2】整体构架【3】上位机开发思路1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献【4】摘要1.4 开发工具的选择【1…...

网络层协议 --- IP
序言 在这篇文章中我们将介绍 IP协议,经过这篇文章的学习,我们就会了解运营商到底是如何为我们提供服务的以及平时我们所说的内网,公网到底又是什么,区别是什么? IP 地址的基本概念 1. IP 地址的定义 每一个设备接入…...

Java虚拟机(JVM)介绍
**Java虚拟机(JVM)**是Java平台的核心组件,它提供了一个运行时环境,使得Java程序可以在不同的操作系统和硬件平台上运行而无需修改。 JVM的架构 JVM主要由以下几个部分组成: 类加载器(Class Loader…...

1000题-计算机网络系统概述
术语定义与其他术语的关系SDU(服务数据单元)相邻层间交换的数据单元,是服务原语的表现形式。在OSI模型中,SDU是某一层待传送和处理的数据单元,即该层接口数据的总和。 - SDU是某一层的数据集,准备传递给下一…...

Authentication Lab | IP Based Auth Bypass
关注这个靶场的其它相关笔记:Authentication Lab —— 靶场笔记合集-CSDN博客 0x01:IP Based Auth Bypass 前情提要 有些开发人员为了图方便,会给站点设置一个 IP 白名单,如果访问站点的用户的 IP 在白名单内,则允许访…...

linux中的火墙优化策略
1.火墙介绍 1. netfilter 2. iptables 3. iptables | firewalld 2.火墙管理工具切换 在rocky9 中默认使用的是 firewalld firewalld -----> iptables dnf install iptables - services - y systemctl stop firewalld systemctl disable firewalld systemctl mask fi…...

GO网络编程(三):海量用户通信系统1:登录功能初步
一、准备工作 需求分析 1)用户注册 2)用户登录 3)显示在线用户列表 4)群聊(广播) 5)点对点聊天 6)离线留言 主界面 首先,在项目根目录下初始化mod,然后按照如下结构设计目录: 海量用户通信系统/ ├── go.mod ├── client/ │ ├──…...

Windows安全加固详解
一、补丁管理 使用适当的命令或工具,检查系统中是否有未安装的更新补丁。 Systeminfo 尝试手动安装一个系统更新补丁。 • 下载适当的补丁文件。 • 打开命令提示符或PowerShell,并运行 wusa.exe <patch_file_name>.msu。 二、账号管…...

JavaScript函数基础(通俗易懂篇)
10.函数 10.1 函数的基础知识 为什么会有函数? 在写代码的时候,有一些常用的代码需要书写很多次,如果直接复制粘贴的话,会造成大量的代码冗余; 函数可以封装一段重复的javascript代码,它只需要声明一次&a…...

云RDS MySQL迁移至本地MySQL
本地准备工作 1.安装:percona-xtrabackup 上传percona-xtrabackup-2.3.9-Linux-x86_64.tar.gz包到/usr/local tar -zxvf percona-xtrabackup-2.3.9-Linux-x86_64.tar.gz mv percona-xtrabackup-2.3.9-Linux-x86_64 percona-xtrabackup 2.创建数据目录 cd /data/ mkdir rds-mys…...

【C++ 11】nullptr 空指针
文章目录 【 0. 问题背景 】0.1 野指针和悬空指针0.2 传统空指针 NULL0.3 传统空指针的局限性 【 1. 基本用法 】【 2. nullptr 的应用 】2.1 nullptr 解决 NULL 的遗留BUG2.2 简单实例 【 0. 问题背景 】 0.1 野指针和悬空指针 总结 野指针悬空指针产生原因指针变量未被初始…...

Flutter + Three.js (WebView)实现桌面端3d模型展示和交互
文章目录 flutter(桌面端)瓶颈一、Flutterthree.js二、Flutterthree.js 实现思路1.在Flutter 中使用webview 进行嵌套2.开启上面嵌套的页面地址2.在含有three.js 的html 中引入模型3.两个页面之间进行通信,如图: 总结 flutter(桌面端)瓶颈 Flutter 本身…...

学习日志35
拆卸线问题(Disassembly Line Balancing Problem, DLBP)是生产工程和运筹学中的一个特殊问题,它涉及到将废弃产品有效地拆解成可回收利用的部件和材料。随着环保意识的增强和资源回收技术的发展,DLBP逐渐成为研究的热点。这类问题…...

http cache-control
Cache-Control 是 HTTP 协议中用于控制缓存行为的重要头部字段。它定义了客户端和服务器端如何缓存资源,以及缓存的有效期。以下是关于 Cache-Control 的详细解释: 请求指令 max-age 指示客户端接受的响应最大年龄。如果缓存的响应超过这个年龄&#x…...