算法学习——回溯算法

回溯算法

  • 理论基础
    • 回溯法的效率
    • 回溯法解决的问题
    • 回溯法模板
  • 组合
    • 思路
      • 回溯法三部曲
    • 代码
  • 组合(优化)
  • 组合总和III
    • 思路
    • 代码
  • 电话号码的字母组合
    • 思路
    • 回溯法来解决n个for循环的问题
    • 回溯三部曲
    • 代码
  • 组合总和
    • 思路
    • 代码
  • 组合总和II
    • 思路
    • 代码

理论基础

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

在二叉树系列中,我们已经不止一次,提到了回溯,例如二叉树:以为使用了递归,其实还隐藏着回溯。
回溯是递归的副产品,只要有递归就会有回溯。

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

回溯法的效率

回溯法的性能如何呢,这里要和大家说清楚了,虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法。
因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

那么既然回溯法并不高效为什么还要用它呢?
因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。

那么都什么问题,这么牛逼,只能暴力搜索。

回溯法解决的问题

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

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

回溯法模板

卡哥总结的回溯算法模板。

回溯三部曲:

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

回溯函数伪代码如下:

void backtracking(参数)

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

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

if (终止条件) {存放结果;return;
}

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

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

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

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

组合

力扣题目链接

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

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

思路

在这里插入图片描述
在这里插入图片描述
可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。

第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。

图中可以发现n相当于树的宽度,k相当于树的深度。

那么如何在这个树上遍历,然后收集到我们要的结果集呢?

图中每次搜索到了叶子节点,我们就找到了一个结果。

相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

回溯法三部曲

递归函数的返回值以及参数
在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。

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

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

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

为什么要有这个startIndex呢?
startIndex 就是防止出现重复的组合。
从下图中红线部分可以看出,在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex。
在这里插入图片描述
所以需要startIndex来记录下一层递归,搜索的起始位置。

那么整体代码如下:

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)

回溯函数终止条件
什么时候到达所谓的叶子节点了呢?

path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
此时用result二维数组,把path保存起来,并终止本层递归。

所以终止条件代码如下:

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

单层搜索的过程
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
在这里插入图片描述
如此我们才遍历完图中的这棵树。
for循环每次从startIndex开始遍历,然后用path保存取到的节点i。
可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。
backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。

代码如下:

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> paths;vector<vector<int>>result;void backtracking(int n , int k ,int startidx){if(paths.size()==k){result.push_back(paths);return;}   for(int i = startidx;i <= n ;i++){paths.push_back(i);backtracking(n,k,i+1);paths.pop_back();}}vector<vector<int>> combine(int n, int k) {backtracking(n,k,1);return result;}
};

组合(优化)

组合总和III

力扣题目链接

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:
所有数字都是正整数。
解集不能包含重复的组合。

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

思路

代码

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

电话号码的字母组合

力扣题目链接

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

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

在这里插入图片描述
示例 :
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]

思路

数字和字母如何映射
可以使用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
};

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

例如:输入:“23”,抽象为树形结构,如图所示:
在这里插入图片描述
图中可以看出遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果,输出[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]。

回溯三部曲

确定回溯函数参数

首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。
再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。
这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。

代码如下:

vector<string> result;
string s;
void backtracking(const string& digits, int index)

确定终止条件
例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。

那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。

然后收集结果,结束本层递归。

代码如下:

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

这里index跳出循环后的大小就是数组的大小

确定单层遍历逻辑

首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。

然后for循环来处理这个字符集,代码如下:

int digit = digits[index] - '0';        // 将index指向的数字转为int
string 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();                       // 回溯
}

注意这里for循环,可不像是在回溯算法:求组合问题和回溯算法:求组合总和中从startIndex开始遍历的。

因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而上述两道题都是求同一个集合中的组合!

代码

class Solution {
public:const vector<string>phones = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};vector<string>result;string path;void backtracking(string digits , int index){if( index == digits.size()){result.push_back(path);return;}int num = digits[index] - '0';string words = phones[num];for(int i = 0 ; i < words.size();++i){path.push_back(words[i]);backtracking(digits,index+1);path.pop_back();}}vector<string> letterCombinations(string digits) {if(digits.size()==0)return result;backtracking(digits,0);return result;}
};

组合总和

力扣题目链接

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。

说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:

输入:candidates = [2,3,6,7], target = 7,
所求解集为: [ [7], [2,2,3] ]

思路

代码

class Solution {
public:vector<vector<int>>result;vector<int>paths;void backtracking(vector<int>& candidates , int target ,int sum , int startidx){if(sum > target){return;}if(sum == target){result.push_back(paths);return ;}for(int i = startidx ; i < candidates.size();++i){paths.push_back(candidates[i]);sum += candidates[i];backtracking(candidates,target,sum,i);sum -= candidates[i];paths.pop_back();}}vector<vector<int>> combinationSum(vector<int>& candidates, int target) {backtracking(candidates,target,0,0);return result;}
};

组合总和II

力扣题目链接

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。

示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:

[[1, 7],[1, 2, 5],[2, 6],[1, 1, 6]
]

思路

如果 candidates 中有重复元素,可能会生成重复的组合。为了避免这种情况,首先需要对 candidates 进行排序。排序后,相同的数字会排列在一起,从而可以更容易地跳过重复的组合。

不排序去重:
在这里插入图片描述

代码

class Solution {
public:vector<vector<int>>result;vector<int>path;void backtracking(vector<int>& candidates, int target, int sum,int startidx ){if(sum > target)return;if(sum==target){result.push_back(path);return;}for(int i = startidx;i<candidates.size();++i){if(i>startidx && candidates[i]==candidates[i-1])continue;path.push_back(candidates[i]);sum += candidates[i];backtracking(candidates,target,sum,i+1);sum -= candidates[i];path.pop_back();}}vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {sort(candidates.begin(),candidates.end());backtracking(candidates,target,0,0);return result;}
};

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

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

相关文章

教师职业规划

教师是一份充满责任和使命感的职业&#xff0c;也是一个具有广泛影响力的职业。作为一名教师&#xff0c;不仅要传授知识&#xff0c;更要培养学生的品德和能力&#xff0c;为他们的未来发展奠定基础。因此&#xff0c;进行职业规划对于教师来说是非常重要的。 首先&#xff0…

VUE篇之可拖动裁剪框

涉及知识点&#xff1a; offsetLeft, offsetTop, offsetWidth, offsetHeight&#xff1b;offsetX, offsetY&#xff1b;clientX&#xff0c;clientY css:clip-path 学习直通车&#xff1a;HTMLElement.offsetLeft - Web API 接口参考 | MDN MouseEvent.offsetX - Web API 接…

利用原始套接字解决mac地址错误问题【南瑞SysKeeper-2000】

一&#xff1a;案例描述 一键可视顺控图像智能项目在网络部署过程中&#xff0c;对网络限制隔离安全性要求很高&#xff0c;用到正向隔离装置&#xff08;南瑞SysKeeper-2000型号&#xff09;。 图一 正向装置示意图 现场发现问题&#xff1a;直连网线情况下&#xff0c;我方…

德人合科技 | 公司电脑文件加密系统

公司电脑文件加密系统是一种可以对电脑文件进行加密的保护机制。它使用驱动层透明加密技术&#xff0c;能够在用户无感知的情况下对文件进行加密&#xff0c;从源头上保障数据安全和使用安全。 PC端访问地址&#xff1a; www.drhchina.com 此类系统主要有以下几个特点和功能&a…

Web前端-JavaScript(js循环)

1.循环 1.1 for循环 语法结构 for(初始化变量; 条件表达式; 操作表达式 ){//循环体 }名称作用初始化变量通常被用于初始化一个计数器&#xff0c;该表达式可以使用 var 关键字声明新的变量&#xff0c;这个变量帮我们来记录次数。条件表达式用于确定每一次循环是否能被执行。…

Git账户密码http方式的配置

Git账户密码http方式的配置 入门 git在提交时每次都需要输入密码和账号信息&#xff0c;可以将账号和密码进行持久化存储&#xff0c; 当git push的时候输入一次用户名和密码就会被记录&#xff0c; 不需要每次输入&#xff0c;提高效率&#xff0c;进行一下配置&#xff1…

深入学习《大学计算机》系列之第1章 1.4节——从二进制起源窥见的奥秘

一.欢迎来到我的酒馆 第1章 1.4节&#xff0c;从二进制起源窥见的奥秘。 目录 一.欢迎来到我的酒馆二.二进制的起源1.关于莱布尼茨2.莱布尼茨和牛顿的恩怨情仇 二.二进制的起源 本节内容属于知识拓展&#xff0c;通过讲解几个小故事&#xff0c;向大家介绍二进制的起源。 1.关…

基于低代码的文档管理系统:实现高效协作与控制

在企业和组织中&#xff0c;文档管理是一项至关重要的任务。文档包括各种类型的信息&#xff0c;如合同、报告、会议记录、产品规格等&#xff0c;它们都需要被妥善保管并确保随时可供查阅。 传统的文档管理方法往往效率低下&#xff0c;且容易出错。随着技术的发展&#xff0…

亚信安慧AntDB数据库引领大数据新纪元,星河案例彰显卓越表现

亚信科技及其附属公司亚信安慧在第六届大数据“星河”案例评选中&#xff0c;凭借其卓越的数据库技术实力&#xff0c;再次站在了行业的聚光灯下。这次的显著成果不仅是对亚信科技技术能力的肯定&#xff0c;更是对其在数据库领域持续创新和领先地位的认可。 图&#xff1a;亚信…

3D小球跑酷

目录 一、前言 二、开发环境 三、场景搭建 1. 创建项目 2. 创建场景内物体 2.1 创建跑道 2.2 创建玩家 2.3 创建障碍物 2.4 改变跑道和障碍物的颜色 2.4.1 创建材质 2.4.2 给跑道和障碍物更换材质 四、功能脚本实现 1. 创建玩家脚本 2. 相机跟随 3. 胜负的判定 3…

单光子如何“玩转”单原子?| 量子简史

在量子力学诞生约100年后的今天&#xff0c;物理学家仍在不断了解光与物质之间的相互作用。 上世纪初&#xff0c;量子力学发展的驱动力之一是人们需要了解为什么原子只能发出特定波长的光。不久之后&#xff0c;量子力学被应用于分子&#xff0c;然后是固体。从另一个方向来看…

Springboot数据加密篇

一、密码加密 1.1Hash算法(MD5/SHA-512等) 哈希算法&#xff0c;又称摘要算法&#xff08;Digest&#xff09;&#xff0c;是一种将任意长度的输入通过散列函数变换成固定长度的输出的单向密码体制。这种映射的规则就是哈希算法&#xff0c;而通过原始数据映射之后得到的二进制…

STM32——串口通信应用篇

一、引言 STM32微控制器是一款功能强大的嵌入式系统芯片&#xff0c;广泛应用于各种领域。其中&#xff0c;串口通信是其重要功能之一&#xff0c;可用于与外部设备进行数据交换和控制。本文将介绍STM32串口通信的基本原理、应用场景以及实现方法。 二、STM32串口通信基本原理 …

三维模型轻量化工具

老子云三维模型服务平台&#xff1a;常规模型轻量化通过底层算法快速有效的对常规模型进行轻量化处理&#xff0c;目前包含两种处理模式&#xff1a;减面模式、合并模式。 减面模式&#xff1a;保留原始模型信息&#xff0c;仅使模型网格更轻量。合并模式&#xff1a;合并模型材…

探秘 AJAX:让网页变得更智能的异步技术(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Linux笔记---网络操作命令详细介绍

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux学习 ⛳️ 功不唐捐&#xff0c;玉汝于成 前言&#xff1a; 网络操作是Linux系统中常见的任务之一&#xff0c;它涵盖了测试网络连接、配置网络接口、显示网络统计信息以及远程登录和文件传…

178. 第K短路(A*启发式算法)

178. 第K短路 - AcWing题库 给定一张 N 个点&#xff08;编号 1,2…N&#xff09;&#xff0c;M 条边的有向图&#xff0c;求从起点 S 到终点 T 的第 K 短路的长度&#xff0c;路径允许重复经过点或边。 注意&#xff1a; 每条最短路中至少要包含一条边。 输入格式 第一行包…

测试工具Jmeter:界面介绍、核心选项说明、核心选项用途

本文章主要介绍Jmeter的界面布局&#xff0c;以及各个选项的功能和它们的用途。 JMeter基本原理是建立一个线程池&#xff0c;多线程运行取样器产生大量负载&#xff0c;在运行过程中通过断言来验证结果的正确性&#xff0c;通过监听器来记录测试结果。 1. Jmeter主界面 当我…

多维时序 | MATLAB实现SSA-CNN-LSTM-Multihead-Attention多头注意力机制多变量时间序列预测

多维时序 | MATLAB实现SSA-CNN-LSTM-Multihead-Attention多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现SSA-CNN-LSTM-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现SSA-CNN-LST…

路由器设置代理IP教程,http代理怎么固定IP地址?

路由器设置代理IP教程 一、确定代理IP地址 首先&#xff0c;你需要确定你要使用的代理IP地址。你可以从代理服务提供商处获取代理IP地址和端口号。 二、登录路由器管理界面 在浏览器中输入路由器的IP地址&#xff0c;输入账号和密码&#xff0c;进入路由器的管理界面。 三、设置…