回溯part011

news/2024/11/15 18:32:36/文章来源:https://www.cnblogs.com/yuehuaicnblogs/p/18372622

今天学习了回溯算法:

  1. 基本知识,关键是那个模板
  2. 组合问题:画树状图+简单的剪枝
  3. 电话号码的组合问题,和经典组合问题的差别在于取不同集合中的组合,注意如何有限制的在for循环之前确定循环哪个数组,通过树状图确定for循环中i的大小。

1. 基本知识

  1. 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。

  2. 本质是穷举,穷举所有可能,然后选出我们想要的答案,所以效率不高。

  3. 解决的问题:

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

    回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

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

2. 77 组合

题目:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

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


a. 如何想到回溯

直观的方法,k=2就是两层循环,k=50就是50层循环嵌套。

回溯可以解决很多层for循环嵌套问题。

回溯问题可以抽象为树形结构,如图:

77.组合
  • 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
  • 图中可以发现n相当于树的宽度,k相当于树的深度。
  • 图中每次搜索到了叶子节点,我们就找到了一个结果。

b. 回溯三部曲

  1. 递归函数的返回值以及参数

在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。

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

函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。

然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] ),防止出现重复的组合。

  1. 回溯函数终止条件

什么时候到达所谓的叶子节点了呢?path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

  1. 单层搜索的过程

for循环每次从startIndex开始遍历,然后用path保存取到的节点i。然后backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。backtracking的下面就是回溯的操作,撤销本次处理的结果。

class Solution {
private:vector<vector<int>> result; // 存放符合条件结果的集合vector<int> path; // 用来存放符合条件结果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(); // 回溯,撤销处理的节点}}
public:vector<vector<int>> combine(int n, int k) {result.clear(); // 可以不写path.clear();   // 可以不写backtracking(n, k, 1);return result;}
};
  • 时间复杂度: O(n * 2^n)

    对于每一个 n,都有两种选择:要么被选中加入到 path 中,要么不被选中。因此,对于 n 个元素,理论上最多有 2^n 种选择方式。最多k=n,所以再乘一个n。

  • 空间复杂度: O(n)

c. 剪枝优化

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

77.组合4

优化过程如下:

  1. 已经选择的元素个数:path.size();
  2. 所需需要的元素个数为: k - path.size();
  3. 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0)n - (k - 0) + 14 - ( 3 - 0) + 1 = 2

从2开始搜索都是合理的,可以是组合[2, 3, 4]。

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

3. 316 组合Ⅲ

题目:找出所有相加之和为 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]]


和上一题的组合异曲同工,需要注意的地方:不是所有叶子节点都要,只要sum=target的结果。所以这个判断在代码的if中。

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

剪枝的方法:

已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。

class Solution {
private: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(); // 回溯}}public:vector<vector<int>> combinationSum3(int k, int n) {result.clear(); // 可以不加path.clear();   // 可以不加backtracking(n, k, 0, 1);return result;}
};
  1. 注意剪枝的两个地方:sum和k
  2. 有了sum,那么每次递归和回溯需要修改sum的值,不是只有path了。

4. 17.电话号码的字母组合

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

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

img

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

要解决如下三个问题:

  1. 数字和字母如何映射

  2. 两个字母就两个for循环,三个字符就三个for循环,明显是多层for循环问题,可以用回溯解决,属于回溯中的组合问题,和经典组合不同的地方在于求不同集合之间的组合,所以每个for循环需要在固定的个数数字中挑一个,画出树状图:

    17. 电话号码的字母组合
  3. 输入1 * #按键等等异常情况

a. 数字和字母如何映射

映射问题,可以使用map或者定义一个二维数组,例如:string letterMap[10],定义一个二维数组,代码如下:

const string letterMap[10] = {"", // 0"", // 1"abc", // 2"def", // 3"ghi", // 4"jkl", // 5"mno", // 6"pqrs", // 7"tuv", // 8"wxyz", // 9
};

b. 回溯法解决n个for循环的问题

  1. 确定回溯函数参数

全局:字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来。

参数:参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。

注意这个index可不是 前两道题中的startIndex了,这个index是记录遍历第几个数字了,即用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。

  1. 确定终止条件

例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。

  1. 确定单层遍历逻辑

首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。然后for循环来处理这个字符集,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 backtracking(const string& digits, int index) {if (index == digits.size()) {result.push_back(s);return;}int digit = digits[index] - '0';        // 将index指向的数字转为intstring letters = letterMap[digit];      // 取数字对应的字符集for (int i = 0; i < letters.size(); i++) {s.push_back(letters[i]);            // 处理backtracking(digits, index + 1);    // 递归,注意index+1,一下层要处理下一个数字了s.pop_back();                       // 回溯}}vector<string> letterCombinations(string digits) {s.clear();result.clear();if (digits.size() == 0) {return result;}backtracking(digits, 0);return result;}
};
  1. 二位数组可以做到数字和字母映射
  2. 递归回溯问题,可以试试画树状图,从而可以看出for循环中循环多少次
  3. 此题中,index很重要,在for循环前,通过index确定取的是二维数组中哪个数字对应的字母
  4. 注意此题空间复杂度,递归问题的空间复杂度和递归层数有关系

今日古诗

八声甘州·记玉关踏雪事清游

张炎〔宋代〕

辛卯岁,沈尧道同余北归,各处杭、越。逾岁,尧道来问寂寞,语笑数日。又复别去。赋此曲,并寄赵学舟。

记玉关踏雪事清游,寒气脆貂裘。傍枯林古道,长河饮马,此意悠悠。短梦依然江表,老泪洒西州。一字无题处,落叶都愁。
载取白云归去,问谁留楚佩,弄影中洲?折芦花赠远,零落一身秋。向寻常、野桥流水,待招来,不是旧沙鸥。空怀感,有斜阳处,却怕登楼。

这首词上片以“记”字领起,气势较为开阔、笔力劲峭。写词人前年冬季赴北写经的旧事,展现了一幅冲风踏雪的北国羁旅图;旧事重提之后,中片则续写北地回归之光景;下片从眼前的离别写起,表现出零落如秋叶的心情。全词先悲后壮,先友情而后国恨,惯穿始终的,是一股荡气回肠的“词气”,词中写身世飘萍和国事之悲感哀婉动人,令人如闻断雁惊风、哀猿啼月。

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

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

相关文章

『模拟赛』暑假集训CSP提高模拟26

『模拟赛记录』暑假集训CSP提高模拟26Rank 打得一般,倒数第二场了。。A. 博弈 直接搬了牛客的一套题。 一眼没思路,模了一会放弃直接去打 T2 了,后来把 \(\mathcal{O(n^2)}\) 暴力 打了拿 30pts。 正解用到了异或哈希。首先确定合法的数量即为总对数 \(\frac{n(n-1)}{2}\) 减…

java变量定义与数据类型

变量:在java程序运行过程中,其值可以发送改变的量 定义变量的语句格式: 数据类型 变量名 = 初始化值; 使用变量的注意事项: 1、使用基本数据类型的变量,实际上使用的是变量中存储的值 2、在同一作用域(同一个大括号)中,变量名不能重复 3、变量名是区分大小写的 4、java…

040、Vue3+TypeScript基础,使用nanoid库生成id

01、使用powershell,输入npm i nanoid来安装: 02、App.vue代码如下:<template><div class="app"><h2 class="title">App.Vue</h2><Page1/><br><Page2/></div> </template><script lang="…

2024.8.21 鲜花

太水了,但还是有重点的Never Gonna Give You Up Were no strangers to love You know the rules and so do I A full commitments what Im thinking of You wouldnt get this from any other guy I just wanna tell you how Im feeling Gotta make you understand Never gonna…

AI大模型应用

参考文档https://creative.chat/1.调用AI大模型API 1.1文心一言 https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application创建应用:https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application 示例代码:https://console…

【Azure Redis】AKS中使用Lettuce连接Redis Cache出现 timed out 问题的解决思路

问题描述 使用 Lettuce 客户端,在AKS环境中连接Azure Redis服务,出现超时错误。 错误消息: Redis command timed out , command timed out after 1 minute(s). 错误截图: 解决思路 当出现Redis客户端连接不上时,需要先排查Redis服务器的状态,比如Server Load是否处于高…

关键字、标识符、注释三剑客

关键字概述 被Java语言赋予特定含义的单词 关键字特点 组成关键字的字母全部小写 关键字注意事项 goto和const作为保留字存在,目前并不使用 类似IDEA这样的集成工具,针对关键字有特殊的颜色标记,非常直观标识符概述 就是给类,接口,方法,变量等起名字时使用的字符序列 组成规则…

三步高考大捷

hi, ____________,见字如晤,展信舒颜。 首先,预祝你考上理想的大学。 当然,这并不容易。正因为有难度,我们才一起引入 OKR 方法论 OKR(Objectives and Key Results, 目标与关键结果)。 大名鼎鼎的 OKR 最早是由 Goolge 这个全球聚集最聪明人的企业管理方法。 后来,人们发…

Python个人收入影响因素模型构建:回归、决策树、梯度提升、岭回归

全文链接:https://tecdat.cn/?p=37423 原文出处:拓端数据部落公众号“你的命运早在出生那一刻起便被决定了。”这样无力的话语,无数次在年轻人的脑海中回响,尤其是在那些因地域差异而面临教育资源匮乏的年轻人中更为普遍。在中国,这种现象尤为明显:没有生在大城市的他们…

1092. 最短公共超序列

非常好的一道理解LCS本质的题目class Solution { public:string longestCommonSubsequence(const string str1, const string str2) {int m = str1.length();int n = str2.length();// 创建一个二维数组来存储LCS的长度vector<vector<int>> dp(m + 1, vector<in…

【转】管理者,一定要有道-法-术的思维模型

我有一个观点:作为管理者,必须要有“道法术”的思维模型。 具备这样的思维,才能穿透表象,站在更高的维度去解决管理上遇到的问题。 今天,我们一起来聊聊这个话题。 术:学工具、学方法 问题来了,什么是术? 王东岳老师说:术,就是看你能驱动什么。 简单来说,术,就是工…