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

news/2024/5/6 21:01:35

本文解决几个问题:

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

一、介绍

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.mrgr.cn/p/36477486

相关文章

wps屏幕录制怎么用?分享使用方法!

数字化时代&#xff0c;屏幕录制已成为我们学习、工作和娱乐中不可或缺的一部分。无论是制作教学视频、分享游戏过程&#xff0c;还是录制网络会议&#xff0c;屏幕录制都能帮助我们轻松实现。WPS作为一款功能强大的办公软件&#xff0c;其屏幕录制功能也备受用户青睐。本文将详…

CentOS-7安装Mysql并允许其他主机登录

一、通用设置&#xff08;分别在4台虚拟机设置&#xff09; 1、配置主机名 hostnamectl set-hostname --static 主机名2、修改hosts文件 vim /etc/hosts 输入&#xff1a; 192.168.15.129 master 192.168.15.133 node1 192.168.15.134 node2 192.168.15.136 node33、 保持服…

day13 ts后端持久层框架(java转ts全栈/3R教室)

简介&#xff1a;如果说TS全栈后端开发最重要的两个框架&#xff0c;除了nestjs就是持久层框架了&#xff0c;这里主要看下Typeorm&#xff08;java中常用的就是mybatis&#xff0c;springdatajpa&#xff0c;hebernite了&#xff09; 先回顾下ORM的概念&#xff1a;ORM就是建…

C# GetField 方法应用实例

目录 关于 C# Type 类 GetField 方法应用 应用举例 心理CT设计题 类设计 DPCT类实现代码 小结 关于 C# Type 类 Type表示类型声明&#xff1a;类类型、接口类型、数组类型、值类型、枚举类型、类型参数、泛型类型定义&#xff0c;以及开放或封闭构造的泛型类型。调用 t…

二叉树的性质

性质一:二叉树的第i层上最多有2^(i-1) 个节点 性质二:深度为k的二叉树最多有2^(k)-1个节点 等比数列求和公式: 直接套进去就得到 2^(k)-1 (结点的度&#xff08;Degree) &#xff1a;结点子树的个数。树的度&#xff1a; 树中结点的最大度数。度为k的树也称为k叉树) 性质三:叶…

Uptime Kuma 使用指南:一款简单易用的站点监控工具

我平时的工作会涉及到监控&#xff0c;而站点是一个很重要的监控项。项目上线后&#xff0c;我们通常会将站点监控配置到云平台上&#xff0c;以检测各站点的连通性。但随着项目不断增多&#xff0c;云平台上的配额就有点捉急了。针对这个情况&#xff0c;我们可以试试这个开源…

CSS画一条虚线,并且灵活设置虚线的宽度和虚线之间的间隔和虚线的颜色

CSS画一条虚线,并且灵活设置虚线的宽度和虚线之间的间隔和虚线的颜色。 先看效果图&#xff1a; 在CSS中&#xff0c;你可以使用border属性或者background属性来画一条虚线。以下是两种常见的方法&#xff1a; 方法一&#xff1a;使用border属性 你可以设置一个元素的border…

4.24日团队开发第五天

今天进行了晨会主要讨论了昨天完成情况,以及遇到的问题 同时针对完成度进行了分析,及时调整了进度

Linux 网络操作命令Telnet

Telnet 尽管 Telnet 已经逐渐被更安全的 SSH 协议所取代&#xff0c;但在某些特定场景下&#xff0c;如对旧系统的维护或教育目的&#xff0c;Telnet 仍然有其使用价值。本文将介绍如何在 Linux 系统中安装 Telnet 客户端&#xff0c;以及如何使用它进行远程登录。 用户使用 t…

MySQL 锁机制全面解析

目录 1. MySQL的锁类型1.1 全局锁1.2 表锁1.3 行锁1.4 共享锁&#xff08;读锁&#xff09;1.5 排它锁&#xff08;写锁&#xff09;1.6 死锁 2 乐观锁和悲观锁2.1 乐观锁2.2 悲观锁 3 意向锁4 间隙锁5 临键锁6 插入意向锁7. 事务隔离级别对锁的影响6.1 读未提交&#xff08;Re…

账号安全及应用

一、账号安全控制 1.1系统账号清理 将用户设置为无法登陆 锁定账户 删除账户 设定账户密码&#xff0c;本质锁定 锁定配置文件-chattr&#xff1a; -a 让文件或目录仅供附加用途。只能追加 -i 不得任意更动文件或目录。 1.2密码安全控制 chage 1.3历史命令 history&am…

OceanBase数据库日常运维快速上手

这里为大家汇总了从租户创建、连接数据库&#xff0c;到数据库的备份、归档、资源配置调整等&#xff0c;在OceanBase数据库日常运维中的操作指南。 创建租户 方法一&#xff1a;通过OCP 创建 确认可分配资源 想要了解具体可分配的内存量&#xff0c;可以通过【资源管理】功…

Hive主要介绍

Hive介绍 hive是基于 Hadoop平台操作 HDFS 文件的插件工具 可以将结构化的数据文件映射为一张数据库表 可以将 HQL 语句转换为 MapReduce 程序 1.hive 是由驱动器组成&#xff0c;驱动器主要由4个组件组成&#xff08;解析器、编译器、优化器、执行器&#xff09; 2.hive本身不…

网络协议深度解析:SSL、 TLS、HTTP和 DNS(C/C++代码实现)

在数字化时代&#xff0c;网络协议构成了互联网通信的基石。SSL、TLS、HTTP和DNS是其中最关键的几种&#xff0c;它们确保了我们的数据安全传输、网页的正确显示以及域名的正常解析。 要理解这些协议&#xff0c;首先需要了解网络分层模型。SSL和TLS位于传输层之上&#xff0c…

数据可视化(四):Pandas技术的高级操作案例,豆瓣电影数据也能轻松分析!

Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&#xff01; 喜欢我的博客的话&#xff0c;记得…

如何在阿里云快速配置自动定时重启ECS云服务器?

背景 无论是电子商务、在线教育、游戏&#xff0c;还是流媒体等业务&#xff0c;服务器的稳定运行都是至关重要的。然而&#xff0c;在实际运行中&#xff0c;我们可能会遇到这样一些场景&#xff1a; 系统更新&#xff1a;一些操作系统或者软件的更新可能需要重启服务器才能…

buuctf-pwn-2.rip

先用checksec看一下保护情况红色表示没有保护,绿色则表示有相应的保护 关于每种保护会在之后的做题中遇到,也有相应的应对措施,这次就不过多深入 打开ida64分析附件发现高危函数gets,这个函数不会检查输入的长度 我们可以利用它修改函数的返回地址,从而执行后门函数找到后…

【draw.io的使用心得介绍】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

条件生成对抗网络(cGAN)在AI去衣技术中的应用探索

随着深度学习技术的飞速发展&#xff0c;生成对抗网络&#xff08;GAN&#xff09;作为其中的一个重要分支&#xff0c;在图像生成、图像修复等领域展现出了强大的能力。其中&#xff0c;条件生成对抗网络&#xff08;cGAN&#xff09;通过引入条件变量来控制生成模型的输出&am…

Redis系列5:深入分析Cluster 集群模式

1 背景 前面我们学习了Redis高可用的两种架构模式&#xff1a;主从模式、哨兵模式。 解决了我们在Redis实例发生故障时&#xff0c;具备主从自动切换、故障转移的能力&#xff0c;终保证服务的高可用。 但是这些其实远远不够&#xff0c;随着我们业务规模的不断扩展&#xff0…