算法学习笔记Day8——回溯算法

本文解决几个问题:

回溯算法是什么?解决回溯算法相关的问题有什么技巧?回溯算法代码是否有规律可循?

一、介绍

1.回溯算法是什么?

回溯算法就是个多叉树的遍历问题,关键在于在前序和后序时间点做一些操作,本质是一种暴力枚举算法,它和DFS非常相似,区别在于,回溯算法关注点在于树的树枝,DFS关注点在于树的节点。

2.回溯算法的技巧

站在一棵决策树的节点,需要考虑三个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

3. 回溯算法的框架(规律)

result = []
def backtrack(路径,选择列表){if(满足结束条件){result.add(路径)return;}for(选择 : 选择列表){//做选择将该选择从选择列表移除路径.add(选择)backtrack(路径, 选择列表)//撤销选择路径.remove(选择)将该选择再加入选择列表}}

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,其实就是在维护每个节点的路径和选择列表信息,

抽象地说,解决一个回溯问题,实际上就是遍历一棵决策树的过程,树的每个叶子节点存放着一个合法答案。你把整棵树遍历一遍,把叶子节点上的答案都收集起来,就能得到所有的合法答案。

我们定义的 backtrack 函数在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层叶子节点,其「路径」就是一个答案。

4. 回溯算法和DFS的关系

回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

i)找到一个可能存在的正确的答案;
ii)在尝试了所有可能的分步方法后宣告该问题没有答案。

深度优先搜索 是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。

5. 回溯算法的拓展

由于回溯算法的时间复杂度很高,因此在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称为 剪枝。剪枝是一种技巧,通常需要根据不同问题场景采用不同的剪枝策略,需要在做题的过程中不断总结。

二、排列、组合、子集相关问题

无论是排列、组合还是子集问题,简单说无非就是让你从序列 nums 中以给定规则取若干元素,主要有以下几种变体:

形式一、元素无重不可复选

形式二、元素可重不可复选

形式三、元素无重可复选

但无论形式怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,所以合理使用回溯算法框架,稍改代码框架即可把这些问题一网打尽

为什么只要记住这两种树形结构就能解决所有相关问题呢?

首先,组合问题和子集问题其实是等价的,这个后面会讲;至于之前说的三种变化形式,无非是在这两棵树上剪掉或者增加一些树枝罢了

无重不可复选

核心:通过保证元素之间的相对顺序不变来防止出现重复的子集。

具体方法:使用 start 参数控制树枝的生长避免产生重复的子集,用 track 记录根节点到每个节点的路径的值,同时在前序位置把每个节点的路径值收集起来,完成回溯树的遍历就收集了所有子集。

例题1:子集

代码

class Solution {
public:vector<vector<int>> ans;vector<int> track;void traceback(vector<int>& nums, int start){// 每个节点的值都是一个子集ans.emplace_back(track);for(int i = start; i < nums.size(); i++){track.emplace_back(nums[i]);traceback(nums, i+1);track.pop_back();}}vector<vector<int>> subsets(vector<int>& nums) {traceback(nums, 0);return ans;}
};

例题2:组合 

分析

代码

class Solution {
public:vector<vector<int>> ans;vector<int> track;void backtrack(int n, int k, int start){if(track.size() == k){ans.emplace_back(track);}for(int i = start; i<= n; i++){track.emplace_back(i);backtrack(n, k, i+1);track.pop_back();}}vector<vector<int>> combine(int n, int k) {backtrack(n, k, 1);return ans;}
};

例题3:全排列

分析

选择列表就是遍历整个数组,如果没有用过某个数字,就选择它,然后把used数组对应的值设为true,然后进入节点(递归调用backtrack()),出来后撤销选择,方式是对路径弹出元素,然后更新used数组。

代码

class Solution {
public:vector<vector<int>> ans;vector<vector<int>> permute(vector<int>& nums) {vector<int> track;vector<bool> used(nums.size(), false);backtrack(nums, track, used);return ans;}void backtrack(vector<int>&nums, vector<int>&track, vector<bool>& used){if(track.size() == nums.size()){ans.push_back(track);return;}for(int i  =0; i < nums.size(); i++){if(used[i] == true){continue;}track.push_back(nums[i]);used[i] = true;backtrack(nums, track, used);track.pop_back();used[i] = false;}}
};

但如果题目不让你算全排列,而是让你算元素个数为 k 的排列,怎么算?

也很简单,改下 backtrack 函数的 base case,仅收集第 k 层的节点值即可

可重不可复选

例题4:子集 II

分析

元素可重的通用解决方法:排序 + used数组,连续相同的元素只能按顺序取,不能前面的没取取后面的。

代码

class Solution {
public:vector<vector<int>> ans;vector<int> track;void backtrack(vector<int>& nums, int start, vector<bool> used){ans.emplace_back(track);for(int i = start; i<nums.size(); i++){// 剪枝逻辑,值相同的相邻树枝,只遍历第一条if(i>0 && nums[i] == nums[i-1] && used[i-1] == false){continue;}track.emplace_back(nums[i]);used[i] = true;backtrack(nums, i+1, used);used[i] = false;track.pop_back();}}vector<vector<int>> subsetsWithDup(vector<int>& nums) {sort(nums.begin(), nums.end());vector<bool> used(nums.size(), false);backtrack(nums, 0, used);return ans;}
};

例题5:组合总和 II

代码

class Solution {
public:vector<int> track;vector<vector<int>> ans;void traceback(vector<int>& candidates, int target, vector<bool>& used, int start){int sum = accumulate(track.begin(), track.end(), 0);if(sum == target){ans.emplace_back(track);return;}if(sum > target){return;}for(int i  = start ; i< candidates.size(); i++){if(used[i] || ( i>0 && candidates[i-1] == candidates[i] && used[i-1] == false)){continue;}used[i] = true;track.emplace_back(candidates[i]);traceback(candidates, target, used, i+1);track.pop_back();used[i] = false;}}vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {sort(candidates.begin(), candidates.end());vector<bool> used(candidates.size(), false);traceback(candidates, target, used, 0);return ans;}
};

例题6:全排列 II

分析

所有相同的数字,他的排列组合只有一个,记住这一点,然后用nums[i-1] == nums[i] && used[i-1] == false这个条件去限制,就可以做到相同数字只有一个排列进入答案。

代码

class Solution {
public:vector<vector<int>> ans;void backtrack(vector<int>& nums, vector<int>& track, vector<bool>& used){if(track.size() == nums.size()){ans.push_back(track);return;}for(int i = 0; i< nums.size(); i++){if(used[i]==true || (i > 0 && nums[i-1] == nums[i] && used[i-1] == false)){continue;}used[i] = true;track.push_back(nums[i]);backtrack(nums, track, used);track.pop_back();used[i] = false;}}vector<vector<int>> permuteUnique(vector<int>& nums) {sort(nums.begin(), nums.end());vector<int> track;vector<bool> used(nums.size(), false);backtrack(nums, track, used);return ans;}
};

元素无重可复选

例题7:组合总和

分析

想解决这种类型的问题,也得回到回溯树上,我们不妨先思考思考,标准的子集/组合问题是如何保证不重复使用元素的

答案在于 backtrack 递归时输入的参数 start,这个 i 从 start 开始,那么下一层回溯树就是从 start + 1 开始,从而保证 nums[start] 这个元素不会被重复使用,那么反过来,如果我想让每个元素被重复使用,我只要把 i + 1 改成 i 即可:

代码

class Solution {
public:vector<vector<int>> ans;vector<int> track;int sum;void backtrack(vector<int>& nums, int target, int start){sum = accumulate(track.begin(), track.end(), 0);if(sum > target){return;}if(sum == target){ans.emplace_back(track);return;}for(int i  = start; i<nums.size(); i++){track.emplace_back(nums[i]);backtrack(nums, target, i);track.pop_back();}}vector<vector<int>> combinationSum(vector<int>& nums, int target) {backtrack(nums, target, 0);return ans;}
};

其他

例题8:排列序列

代码

思路1:回溯 + 剪枝

class Solution {
public:vector<vector<char>> ans;vector<char> track;int cnt = 0;void backtrack(int n, vector<bool> used){if(track.size() == n){ans.emplace_back(track);cnt++;return;}for(int i  = 0; i< n; i++){if(used[i]){continue;}track.emplace_back(i+1+'0');used[i] = true;backtrack(n, used);used[i] = false;track.pop_back();}}string getPermutation(int n, int k) {vector<bool> used(n, false);//如果数字过大,直接定位第一位if( k > 120){int factorial = 1;int first;for(int i = 1; i < n; i++){factorial *= i;}first = k/factorial;k %= factorial;track.emplace_back(first + 1 + '0');used[first] = true;}//业务逻辑backtrack(n, used);string ret(ans[k-1].begin(), ans[k-1].end());return ret;}
};

 思路2:直接定位

class Solution {
public:string getPermutation(int n, int k) {vector<char> ans, array;int tmp, ncp = n, f = 1;array.emplace_back('1');for(int i = 1; i < ncp; i++){f*= i;array.emplace_back(i+1+'0');}f*= ncp;k--;while(n != 0){f /= n--;tmp = k/f;k %= f;ans.emplace_back(array[tmp%ncp]);array.erase(array.begin() + tmp);}string ret(ans.begin(), ans.end());return ret;}
};

例题9:复原 IP 地址

代码

class Solution {
public:vector<string> ans;vector<int> segment;void backtrack(string s,int pos, int segcnt){int segnum = 0;if(pos == s.size() && segcnt  == 4){string track;for(int i = 0; i< 4; i++){track += to_string(segment[i]);if(i != 3) track += '.';}ans.emplace_back(track);//到达末尾return回去return;}else if((segcnt == 4 && pos != s.size()) || (pos == s.size() && segcnt != 4)){return;}if(s[pos] == '0'){segment[segcnt] = 0;backtrack(s, pos + 1, segcnt + 1);//撤销选择的方法是returnreturn;}for(int i  = pos; i<s.size(); i++){segnum  = segnum*10 + (s[i] - '0');if(segnum > 0 && segnum <= 0xFF){segment[segcnt] = segnum;backtrack(s, i+1, segcnt + 1);}else{return;}}}vector<string> restoreIpAddresses(string s) {segment.resize(4);backtrack(s, 0, 0);return ans;}
};

三、Flood Fill(DFS)

例题10:单词搜索

分析

本题说是回溯,其实很像DFS的方法,参考:

DFS 算法解决岛屿题目

代码

class Solution {
public:int m, n;bool backtrack(vector<vector<char>>& board, string word, int pos, int i, int j){if(i >= m || j >= n || i<0 || j<0 || board[i][j] != word[pos]){return false;}if(pos == word.size()-1){return true;}bool res;board[i][j] = 0;res = backtrack(board, word, pos+1, i+1, j) || backtrack(board, word, pos+1, i-1, j)||backtrack(board, word, pos+1, i, j+1) || backtrack(board, word, pos+1, i, j-1);board[i][j] = word[pos];return res;}bool exist(vector<vector<char>>& board, string word) {m = board.size();n = board[0].size();for(int i = 0; i< m; i++){for(int j = 0; j<n; j++){if(backtrack(board, word, 0, i, j))return true;}}return false;}
};

例题11:被围绕的区域

DFS 算法解决岛屿题目 中有详解

例题12:岛屿数量

DFS 算法解决岛屿题目 中有详解

例题13:​​​​​​​图像渲染

分析

dfs模板:越界返回

                访问返回

                非目标返回

                dfs递归

代码

class Solution {
public:int m, n;int samecolor;void dfs(vector<vector<int>>& image, int i, int j, int color){if(i>=m || j>= n|| i<0 || j<0){return;}if(image[i][j] == color){return;}if(image[i][j] != samecolor){return;}image[i][j] = color;dfs(image, i+1, j, color);dfs(image, i, j+1, color);dfs(image, i, j-1, color);dfs(image, i-1, j, color);}vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {m = image.size(); n = image[0].size();samecolor = image[sr][sc];dfs(image, sr, sc, color);return image;}
};

 

三、字符串中的回溯问题

例题14:电话号码的字母组合

代码

class Solution {
public:vector<string> ans;string track;vector<string>array;void backtrack(string digits, int pos){if(track.size() == digits.size()){ans.emplace_back(track);return;}//根据数字算出其对应的选择有哪些for(char c : array[digits[pos]-'0']){track += c;backtrack(digits, pos+1);track.erase(track.end()-1);}}vector<string> letterCombinations(string digits) {if(digits.size() == 0){return ans;}array.resize(10);for(int i = 0; i<18; i++){array[i/3+2] += i+'a';}array[7] += 's'; array[8] = "tuv"; array[9] = "wxyz";backtrack(digits, 0);return ans;}
};

改良:这里用hashmap存储选择数组比较好

class Solution {
public:vector<string> ans;string track;unordered_map<char, string> array;void backtrack(string digits, int pos){if(track.size() == digits.size()){ans.emplace_back(track);return;}for(char c : array[digits[pos]]){track += c;backtrack(digits, pos+1);track.erase(track.end()-1);}}vector<string> letterCombinations(string digits) {if(digits.size() == 0){return ans;}array = {{'2', "abc"},{'3', "def"},{'4', "ghi"},{'5', "jkl"},{'6', "mno"},{'7', "pqrs"},{'8', "tuv"},{'9', "wxyz"}};backtrack(digits, 0);return ans;}
};

 

例题15:字母大小写全排列

代码

class Solution {
public:vector<string> ans;string track;string choice(char c){string ret;ret += c;if(c >= 'a' && c <= 'z'){ret += toupper(c);}if(c >= 'A' && c <= 'Z'){ret += tolower(c);}return ret;}void backtrack(string s, int pos){if(track.size() == s.size()){ans.emplace_back(track);return;}//做选择for(char c : choice(s[pos])){track += c;backtrack(s, pos+1);track.erase(track.end()-1);}}vector<string> letterCasePermutation(string s) {backtrack(s, 0);return ans;}
};

例题16:括号生成

分析

代码

三、游戏问题

例题17:N 皇后

分析

代码

例题18:解数独

分析

代码

例题19:​​​​​​​祖玛游戏

分析

代码

例题20:​​​​​​​扫雷游戏

分析

代码

四、总结

i)全排列解决方法:

用used数组来选取没有选过的元素

ii)元素可重的通用解决方法

排序 + used 数组,连续相同的元素只能按顺序取,不能前面的没取取后面的。

iii)组合/子集(非排列)解决方法

backtrack传start进去,控制取元素的顺序,避免重复访问。

可重复选:递归的时候传 i

不可重复选:递归的时候传 i + 1

比如,[1, 2, 3],固定一的时候[1,3]取走了,下一次固定3,如果不控制顺序,还会取到[3,1],这样按组合的逻辑来说就是重复的。

iii)只要从树的角度思考,这些问题看似复杂多变,实则改改 base case 就能解决

iv)vector<char>转化为string:直接string str(v.begin(), v.end());

v)string删去最后一个元素: string.erase(string.end()-1);

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

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

相关文章

Java基础入门day35

day35 js 简介 js&#xff1a;JavaScript&#xff0c;是一种解释性语言&#xff0c;动态类型、弱类型的计算机语言 它的解释器被称之为JavaScript引擎&#xff0c;作为浏览器的一部分&#xff0c;广泛用于客户端脚本语言&#xff0c;用来给html网页增加动态功能 问题描述&…

哈希表练习题

前言 本次博客将要写一写&#xff0c;哈希表的一些使用 哈希表主要是一个映射&#xff0c;比如数组就是一个哈希表 是一个整型对应另一个整型&#xff0c;介绍的哈希表还是要以写题目为例 第一题 242. 有效的字母异位词 - 力扣&#xff08;LeetCode&#xff09; 直接来看…

chrome插件 脚本 使用和推荐

chrome插件使用 在极简插件中可以进行下载并进行安装, 内部有安装教程在极简插件中搜索"油猴",下载一个油猴插件,并安装,可以用于下载很多的用户脚本用户脚本下载地址Greasy Fork,里面有很多实用的用户脚本供下载,并在油猴中进行管理 推荐的插件 Tampermonkey 篡改…

小红书自动互动,建立个人品牌的秘密武器!

在数字化的今天&#xff0c;个人品牌的重要性不言而喻。它不仅能让你在人群中脱颖而出&#xff0c;还能为你的事业或生意带来无尽的机会。然而&#xff0c;建立并推广个人品牌并非易事&#xff0c;需要策略、耐心和一定的工具辅助。在这里&#xff0c;我们要探讨的是如何利用小…

【Python数据库】Redis

文章目录 [toc]数据插入数据查询数据更新数据删除查询存在的所有key 个人主页&#xff1a;丷从心 系列专栏&#xff1a;Python数据库 学习指南&#xff1a;Python学习指南 数据插入 from redis import Redisdef insert_data():redis_cli Redis(hostlocalhost, port6379, db…

智慧健康旅居养老产业,做智慧旅居养老服务的公司

随着社会的进步和科技的飞速发展&#xff0c;传统的养老模式已经无法满足 现代老年人的多元化 需求。智慧健康旅居养老产业应运而生&#xff0c;成为了一种新型的养老模式&#xff0c;旨在为老年人提供更加舒适、便捷、安全的养老生活。随着社会的进步和人口老龄化趋势的加剧&a…

如何3分钟,快速开发一个新功能

背景 关于为什么做这个代码生成器&#xff0c;其实主要有两点: 参与的项目中有很多分析报表需要展示给业务部门&#xff0c;公司使用的商用产品&#xff0c;或多或少有些问题&#xff0c;这部分可能是历史选型导致的&#xff0c;这里撇开不不谈&#xff1b;项目里面也有很多C…

Sping源码(七)—context: component-scan标签如何扫描、加载Bean

序言 简单回顾一下。上一篇文章介绍了从xml文件context component-scan标签的加载流程到ConfigurationClassPostProcessor的创建流程。 本篇会深入了解context component-scan标签底层做了些什么。 component-scan 早期使用Spring进行开发时&#xff0c;很多时候都是注解 标…

项目上线流程(保姆级教学)

01&#xff1a;注册阿里云账户 02&#xff1a;登录阿里云 03&#xff1a;在桌面新建记事本保存个人账号密码等信息 04&#xff1a;完成重置密码 05&#xff1a;安装宝塔面板 命令行 yum install -y wget && wget -O install.sh http://download.bt.cn/install/instal…

大学生在线考试|基于SprinBoot+vue的在线试题库系统系统(源码+数据库+文档)

大学生在线考试目录 基于SprinBootvue的在线试题库系统系统 一、前言 二、系统设计 三、系统功能设计 试卷管理 试题管理 考试管理 错题本 考试记录 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#…

Java数据结构堆

堆的概念 所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中。 小根堆&#xff1a;根节点的大小小于孩子节点。整棵树都是小根堆必须满足每颗子树都是小根堆。 堆的存储方式 从堆的概念可知&#xff0c;堆是一棵完全二叉树&#xff0c;因此可以层序的规则采用顺序的…

【JVM】java内存区域

目录 一、运行时数据区域 1、方法区 2、堆 3、虚拟机栈 4、本地方法栈 5、程序计数器 6、运行时常量池 二、HotSpot虚拟机的对象 1、对象的创建 指针碰撞&#xff1a; 空闲列表&#xff1a; 2、对象的内存布局 对象头 实例数据 对齐填充 3、对象的访问定位 句…

git忽略文件配置 !

.gitignore中!表示取反 注意&#xff0c;如果父目录被排除&#xff0c;则父目录下的子目录也会被排除&#xff0c;此时对父目录下的子目录取反也不会生效&#xff0c;比如存在目录结构&#xff0c;再.gitignore目录下配置的 /*&#xff08;排除所有文件&#xff09;&#xff0c…

【LLM多模态】Qwen-VL模型结构和训练流程

note 观点&#xff1a;现有很多多模态大模型是基于预训练&#xff08;和SFT对齐&#xff09;的语言模型&#xff0c;将视觉特征token化并对齐到语言空间中&#xff0c;利用语言模型得到多模态LLM的输出。如何设计更好的图像tokenizer以及定位语言模型在多模态LLM中的作用很重要…

面试算法题之暴力求解

这里写目录标题 1 回溯1.1 思路及模板1.1 plus 排列组合子集问题1.2 例题1.2.1 全排列1.2.2 N 皇后1.2.3 N皇后问题 II1.2.4 子集 &#xff08;子集/排列问题&#xff09;1.2.4 组合(组合/子集问题)1.2.5 全排列 &#xff08;排列问题&#xff09;1.2.1做过1.2.6 子集II &#…

项目十一:爬取热搜榜(小白实战级)

首先&#xff0c;恭喜各位也恭喜自已学习爬虫基础到达圆满级&#xff0c;今后的自已python爬虫之旅会随着网络发展而不断进步。回想起来&#xff0c;我学过请求库requests模块、解析库re模块、lmxl模块到数据保存的基本应用方法&#xff0c;这一次的学习python爬虫之旅收获很多…

模块三:二分——153.寻找旋转排序数组中的最小值

文章目录 题目描述算法原理解法一&#xff1a;暴力查找解法二&#xff1a;二分查找疑问 代码实现解法一&#xff1a;暴力查找解法二&#xff1a;CJava 题目描述 题目链接&#xff1a;153.寻找旋转排序数组中的最小值 根据题目的要求时间复杂度为O(log N)可知需要使用二分查找…

vue集成百度地图vue-baidu-map

文章目录 vue集成百度地图vue-baidu-map1. Vue Baidu Map文档地址2. 设置npm数据源3. 安装vue-baidu-map4. 配置vue-baidu-map4.1 main.js全局注册4.2 vue页面设置4.3 效果 vue集成百度地图vue-baidu-map 1. Vue Baidu Map文档地址 https://dafrok.github.io/vue-baidu-map/#…

Golang GMP解读

概念梳理 1. 1 线程 通常语义中的线程&#xff0c;指的是内核级线程&#xff0c;核心点如下&#xff1a; 是操作系统最小调度单元&#xff1b;创建、销毁、调度交由内核完成&#xff0c;cpu 需完成用户态与内核态间的切换&#xff1b;可充分利用多核&#xff0c;实现并行. …

Unity之圆环slider

一、参考文章 Unity_圆环滑动条&#xff08;圆形、弧形滑动条&#xff09;_unity弧形滑动条-CSDN博客 此滑动条拖动超过360后继续往前滑动值会从0开始&#xff0c;正常我们超过360度时不可在滑动。 二、 超过360度不可滑动问题解决 参考HTML文章制作&#xff1a; https://www.c…