1.DFS 和回溯算法区别
DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置
2.何时使用回溯算法
当问题需要 "回头",以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止
3.怎么样写回溯算法(从上而下,※代表难点,根据题目而变化)
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择
4.回溯问题的类型
5.例题及个人理解
子集类题目,答案从空集开始,集合中包含一个元素的,集合中包含俩个元素的.......集合中包含给定数组个数个元素的,所以用了一个for循环,设置一个 level 变量,值为从0开始一直到给定数组个数作为限制该子集中只能包含 level 个元素
子集问题的选择列表,是上一条选择路径之后的数,所以设置变量 start 表示每次循环递归的起始位置,每次循环都让起始位置 start + 1 以达到从上一条选择路径之后的数。
递归结束条件:新子集数组的个数满足当前循环限制的该子集中只能包含 level 个元素 即 新子集数组个数 等于 该层循环 level
var subsets = function (nums) {
const res = [];
// path:每一个子集数组,level:该层子集集合应该有的个数 start:从原集合中第几个位置开始
const backtrack = (path, level, start) => {
if (path.length === level) {
res.push(path);
return;
}
// 每次循环递归都让起始位置+1 即 i+1
for (let i = start; i < nums.length; i++) {
backtrack(path.concat(nums[i]), level, i+1);
}
}
// i 从 0 开始 i等于0的时候就是空集
for (let i = 0; i <= nums.length; i++) {
// 子集数组,该层数组应有几个元素 从原数组第几个位置开始
backtrack([], i, 0);
}
return res;
};
因为给定集合中有重复的元素,所以核心是去重,所谓去重,其实就是使用过的元素不能重复选取,“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
为了实现去重,就要先保证给定的集合数组是有序的,所以要先排序
裁剪:由递归树可以看出,求有重复元素集合的子集,就是在上一题基础上额外删去同一树层上的重复集合,即在遍历当前选择列表当中,如果后一个列表元素与前一个列表元素相同,则不做新的添加。
而循环递归时指定从原集合第几个元素开始(start参数)该次递归的选择列表就是原集合从 start开始到集合最后一个元素。
想有前后选择列表元素的比较,那么最少也是循环到了选择列表的第二个元素了。即 i > start
递归结束条件:新子集数组的个数满足当前循环限制的该子集中只能包含 level 个元素 即 新子集数组个数 等于 该层循环 level
var subsetsWithDup = function (nums2) {
// 保证给定集合数组有序
const nums = nums2.sort((a, b) => {
return a - b;
})
const res = [];
const backtrack = (path, level, start) => {
if (path.length === level) {
res.push(path);
return;
}
for (let i = start; i < nums.length; i++) {
// 满足后一个选择列表元素等于前一个选择列表元素,则不做添加操作,直接跳过
if (i > start && nums[i] === nums[i - 1]) {
continue;
}
backtrack(path.concat(nums[i]), level, i + 1);
}
}
for (let i = 0; i <= nums.length; i++) {
// 数组,限制该子集中有几个元素,循环开始下标
backtrack([], i, 0);
}
return res;
};
组合的题目是允许重复使用给定集合中的元素的,只要最终结果能够满足给定的 target 值,不限制用了多少个子元素和重复使用了几次子元素。
因为要计算不断添加元素的总和是否已经满足 target 所以每次循环递归还要传递一个 sum 参数
递归函数代码的整体模板与上俩题类似,区别在于本题允许重复使用元素且无需限制子集的元素个数,所以递归循环起始元素不需要每次 + 1,就从上一个元素开始继续递归循环
裁剪:sum 的值大于 target 时,不进行任何操作
递归结束条件:sum 的值等于 target 时 推入 res 数组
var combinationSum = function (candidates, target) {
const res = [];
let sum = 0;
// 路径数组,当前数组总和,起始位置,目标值
const backtrack = (path, sum, start, target) => {
if (sum >= target) {
if (sum === target) {
res.push(path);
}
return;
}
for (let i = start; i < candidates.length; i++) {
// 因为允许重复 所以start参数的值不需要额外 +1 直接从当前元素开始继续循环
backtrack(path.concat(candidates[i]), sum + candidates[i], i, target)
}
}
backtrack([], sum, 0, target);
return res;
};