> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。
> 目标:了解什么是穷举vs暴搜vs深搜vs回溯vs剪枝,并且掌握其算法。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:递归、搜索与回溯算法_დ旧言~的博客-CSDN博客
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
一、算法讲解
什么是回溯算法:
- 回溯算法是⼀种经典的递归算法,通常⽤于解决组合问题、排列问题和搜索问题等。
- 回溯算法的基本思想:从⼀个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态⽆法前进时,回退到前⼀个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护⼀个状态树,通过遍历状态树来实现对所有可能解的搜索。
- 回溯算法的核⼼思想:"试错",即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索;否则,回退到上⼀个状态,重新做出选择。回溯算法通常⽤于解决具有多个解,且每个解都需要搜索才能找到的问题。
回溯算法的模板:
cpp
void backtrack(vector<int>& path, vector<int>& choice, ...) {
// 满⾜结束条件
if (/* 满⾜结束条件 */) {
// 将路径添加到结果集中
res.push_back(path);
return;
}
// 遍历所有选择
for (int i = 0; i < choices.size(); i++) {
// 做出选择
path.push_back(choices[i]);
// 做出当前选择后继续搜索
backtrack(path, choices);
// 撤销选择
path.pop_back();
}
}
其中, path 表⽰当前已经做出的选择, choices 表⽰当前可以做的选择。在回溯算法中,我们需
要做出选择,然后递归地调⽤回溯函数。如果满⾜结束条件,则将当前路径添加到结果集中;否则,我们需要撤销选择,回到上⼀个状态,然后继续搜索其他的选择。
回溯算法的时间复杂度通常较⾼,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护⼀个状态树。在实际应⽤中,回溯算法通常需要通过剪枝等⽅法进⾏优化,以减少搜索的次数,从⽽提⾼算法的效率。
总结:
回溯算法是⼀种⾮常重要的算法,可以解决许多组合问题、排列问题和搜索问题等。回溯算法的核⼼思想是搜索状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的模板⾮常简单,但是实现起来需要注意⼀些细节,⽐如如何做出选择、如何撤销选择等。
二、算法习题
2.1 第一题
题目链接: 46. 全排列 - 力扣(LeetCode)
题目描述:
算法思路:
典型的回溯题⽬,我们需要在每⼀个位置上考虑所有的可能情况并且不能出现重复。通过深度优先搜索的⽅式,不断地枚举每个数在当前位置的可能性,并回溯到上⼀个状态,直到枚举完所有可能性,得到正确的结果。
每个数是否可以放⼊当前位置,只需要判断这个数在之前是否出现即可。具体地,在这道题⽬中,我们可以通过⼀个递归函数 backtrack 和标记数组 visited 来实现全排列。
递归函数设计:
void backtrack(vector<vector<int>>& res, vector<int>& nums, vector<bool>& visited, vector<int>& ans, int step, int len)
- 参数:step(当前需要填⼊的位置),len(数组⻓度);
- 返回值:⽆;
- 函数作⽤:查找所有合理的排列并存储在答案列表中。
递归流程如下:
-
⾸先定义⼀个⼆维数组 res ⽤来存放所有可能的排列,⼀个⼀维数组 ans ⽤来存放每个状态的排列,⼀个⼀维数组 visited 标记元素,然后从第⼀个位置开始进⾏递归;
-
在每个递归的状态中,我们维护⼀个步数 step,表⽰当前已经处理了⼏个数字;
-
递归结束条件:当 step 等于 nums 数组的⻓度时,说明我们已经处理完了所有数字,将当前数组存⼊结果中;
-
在每个递归状态中,枚举所有下标 i,若这个下标未被标记,则使⽤ nums 数组中当前下标的元
素:
- 将 visited[i] 标记为 1;
- ans 数组中第 step 个元素被 nums[i] 覆盖;
- 对第 step+1 个位置进⾏递归;
- 将 visited[i] 重新赋值为 0,表⽰回溯;
- 最后,返回 res。
- 特别地,我们可以不使⽤标记数组,直接遍历 step 之后的元素(未被使⽤),然后将其与需要递归的位置进⾏交换即可。
代码呈现:
cpp
class Solution {
vector<vector<int>> ret;
vector<int> path;
bool check[7];
public:
vector<vector<int>> permute(vector<int>& nums)
{
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if (path.size() == nums.size())
{
ret.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++)
{
if (!check[i])
{
path.push_back(nums[i]);
check[i] = true;
dfs(nums);
// 回溯 -> 恢复现场
path.pop_back();
check[i] = false;
}
}
}
};
2.2 第二题
题目链接: 78. 子集 - 力扣(LeetCode)
题目描述:
算法思路:
为了获得 nums 数组的所有⼦集,我们需要对数组中的每个元素进⾏选择或不选择的操作,即 nums数组⼀定存在 2^(数组⻓度) 个⼦集。对于查找⼦集,具体可以定义⼀个数组,来记录当前的状态,并对其进⾏递归。
对于每个元素有两种选择:1. 不进⾏任何操作;2. 将其添加⾄当前状态的集合。在递归时我们需要保证递归结束时当前的状态与进⾏递归操作前的状态不变,⽽当我们在选择进⾏步骤 2 进⾏递归时,当前状态会发⽣变化,因此我们需要在递归结束时撤回添加操作,即进⾏回溯。
递归函数设计:
void dfs(vector<vector<int>>& res, vector<int>& ans, vector<int>& nums, int step)
- 参数:step(当前需要处理的元素下标);
- 返回值:⽆;
- 函数作⽤:查找集合的所有⼦集并存储在答案列表中。
递归流程如下:
-
递归结束条件:如果当前需要处理的元素下标越界,则记录当前状态并直接返回;
-
在递归过程中,对于每个元素,我们有两种选择:
- 不选择当前元素,直接递归到下⼀个元素;
- 选择当前元素,将其添加到数组末尾后递归到下⼀个元素,然后在递归结束时撤回添加操作;
- 所有符合条件的状态都被记录下来,返回即可。
代码呈现:
cpp
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums)
{
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos)
{
if (pos == nums.size())
{
ret.push_back(path);
return;
}
// 选
path.push_back(nums[pos]);
dfs(nums, pos + 1);
path.pop_back(); // 恢复现场
// 不选
dfs(nums, pos + 1);
}
};
三、结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。