目录
穷举vs暴搜vs深搜vs回溯vs剪枝
这些名词最开始学算法的时候看着可能一头雾水,但是本篇文章已经是第15篇了,这一路下来可以发现,上面这些名词其实很不是很神秘,深搜就是二叉树递归,回溯就是往回走,剪枝就是加几个判断而已
下面我们通过两非常典型的回溯的例题来全面搞懂这些东西
46.全排列
题目很简单,就是让我们返回全排列的结果,这道题题目并不含有重复数字,含有重复数字的是 “全排列 Ⅱ”,这个我们后面再讲,下面咱们来分析下这道题:
- 这一道题本质是穷举类的题,我们前面已经见到过很多,最简单的解法就是用两层循环暴力枚举;而这道题,就是数组中有多少个数就用多少个循环,像示例一有3个数就用三个循环。但是问题来了,万一题目给的数组有100个数呢?那就要用100个循环,显然会超时
- 首先是第一步,我们先画一个详细的决策树,以示例一为例,如下图:
- 当画出决策树并找到相同子问题时,我们的决策树就可以翻译成代码
- 就和上一篇博客一样,我们先来设计下“全局变量”: vector<vector<int>> vv,作用是保存最终的结果;vector<int> path,这个就是在我们深度优先遍历的时候,记录下路径,也就是保存每一次全排列的结果
- 模拟的过程也很简单,以上面决策树的最终结果为123的那一条路径为例,当path的长度等于题目数组的长度时,完成一次全排列,然后回溯,干掉 “123” 的后面两个数,“恢复现场”为“1 __ __”然后往右走3的路径。这个步骤简单,但是我们要如何处理“剪枝”呢?
- 所以我们还需要一个全局变量:bool[] check,是一个bool类型的数组,作用是判断一个数在一个路径里已经是否被使用,当“1 __ __” 选择 2 时,就把check[1] 设置为 true,意思就是 1 位置的这个数 “2” 已经被使用过了,后面每次枚举时判断一下,在 check中找找有没有用过,就可以实现剪枝
- 然后就是设计 dfs 函数,也很简单,只需关心某一个节点在干什么事情,就是把数组中所有的数枚举一遍,只要检查check有没有用过,用过就是返回,没用过旧添加进path后面
细节处理:
- 回溯:当path往后返回时,把最后一个数干掉;并且还要把 check 里的该位置的true改为false
- 剪枝:check实现
- 递归出口:遇到叶子节点时,直接添加结果即可,然后返回
class Solution {
public:
vector<vector<int>> vv;
vector<int> path;
bool check[7];
vector<vector<int>> permute(vector<int>& nums)
{
dfs(nums);
return vv;
}
void dfs(vector<int>& nums)
{
if(nums.size() == path.size()) //遇到叶子节点就添加结果
{
vv.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++)
{
if(check[i] == false)
{
path.push_back(nums[i]);
check[i] = true;
dfs(nums);
//回溯(恢复现场):和上面的反着来即可
path.pop_back();
check[i] = false;
}
}
}
};
78. 子集
这道题考察的知识点就是我们高中时学的子集问题,但这个题目有点不一样,比如[1, 2, 3] 和 [2, 3, 1] 是同一个子集,因为所含数字相同,所以要注意区分,下面来分析下这道题:
解法一
- 解法步骤和上面是一样的:①搞出一个决策树 ②设计全局变量和dfs函数主题 ③处理细节问题
- 先来搞一个决策树,如下图:
- 然后就是设计全局变量,vector<int> path,作用是记录每个路径选或者不选之后的一个结果,vector<vector<int>> vv,用来记录最终结果,而且通过上面的决策树可以发现,这道题并没有剪枝操作
- 再然后就是dfs的函数体设计,dfs[nums, i],我们不仅仅要知道当前这个数组,还要告诉我接下来要选的是哪个位置的数,所以dfs要干的事就是判断 “ i ” 这个数选还是不选即可
- 如果要选,就是 path += nums[i] 后dfs到下一层;如果不选,就啥也不干,直接dfs到下一层
细节处理:
- 回溯:如果选择 i 加入,加入后dfs没问题;但是当dfs完回溯的时候,需要把path最后的值干掉,也就是 dfs -= nums[i]
- 递归出口: 当 i == nums.size() 时,把path扔 vv 里去即可
class Solution {
public:
vector<vector<int>> vv;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums)
{
dfs(nums, 0);
return vv;
}
void dfs(vector<int>& nums, int i)
{
if(nums.size() == i)
{
vv.push_back(path);
return;
}
//选择这个数
path.push_back(nums[i]);
dfs(nums, i + 1);
path.pop_back(); //恢复现场
//不选,直接递归
dfs(nums, i + 1);
}
};
解法二
- 步骤也是:①画决策树 ②设计全局变量和dfs函数体 ③细节问题
- 解法二的决策树的选择策略,就是依据元素个数来选,如下图:
- 全局变量和解法一的vv和path一样
- 然后就是dfs的函数体,dfs[nums, pos],pos表示数组下标,含义就是“下一次添加数字时,从数组的哪里开始”,每枚举出一个结果,就把该结果扔 vv 里去,然后从i + 1的位置继续dfs
细节处理:
- 刚开始进到函数体的时候,直接收集结果
class Solution {
public:
vector<vector<int>> vv;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums)
{
dfs(nums, 0);
return vv;
}
void dfs(vector<int>& nums, int pos)
{
//解法二,每次刚开始,path都是一个结果
vv.push_back(path);
for(int i = pos; i < nums.size(); i++)
{
path.push_back(nums[i]);
dfs(nums, i + 1);
path.pop_back();
}
}
};