本章记录一些有关回溯算法的一些较为经典或者自己第一次做印象比较深刻的算法以及题型,包含自己作为初学者第一次碰到题目时想到的思路以及网上其他更优秀的思路,本章持续更新中......
回溯算法:回溯算法其实本质上是一种暴力穷举的算法,一听到暴力穷举,第一感觉就是效率不高,那为什么还是要使用回溯算法呢?因为有一些问题的规模是非常大的,一个 for 循环可以写,2个 for循环嵌套也还行,3个 for循环嵌套也忍了,那有的问题需要10个、100个 for循环嵌套呢,总不能手撸 N 个 for 循环嵌套吧,这时候回溯算法就排上了用场。回溯就是一个递归函数,也就是自己调用自己,我们设置终止条件,当满足终止条件的时候就结束。回溯和递归总是一起出现的,要理解回溯和递归,其实光靠想象有点难度,最好可以画一个图来把递归回溯的过程形式化展现出来,其实可以用树状图来表示一个递归回溯的过程。
递归过程可以用N叉树表示,每一层表示当前可以做的选择,比如对一个数组 [1,2,3,4] 进行递归,那第一层就是 1,2,3,4;然后在分别在排除这些元素的剩余集合中继续递归,比如,此时节点 1 下面的一层可能是2 3 4,节点 2 下面的一层是 1 3 4。
递归的要素:1、终止条件:递归必须要有终止条件或者能够自动返回,否则很容易导致无限递归从而导致栈溢出。2、for循环:利用for循环是来横向遍历的,而递归是纵向遍历,可以理解为利用for循环实现广度优先遍历,利用递归实现深度优先遍历,同时进行。3、递归参数:递归的参数在递归的时候需要用什么就写什么就好。
目录
No 77. 组合(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combinations/
题目描述:
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
思路:用这个题来整理一下回溯问题的模板。题目的意思其实就是找组合,组合的元素个数是 K。组合无序,所以使用过的数字不能再使用,否则就重复了,所以越往后找其实需要遍历的越少。
递归函数参数:需要三个,分别是起始位置、要求的区间末尾数字、组合的大小。凡是组合类的问题,都需要一个起始位置作为参数,因为下一个递归需要从下一个位置开始。递归函数一般不需要返回值,但也有例外情况,有的情况加上一个返回值会提高搜索效率。
递归的逻辑:把当前的数字添加到结果中,然后在递归下一个数字。当递归返回时再弹出。
递归终止条件:只要当前的结果大小等于 K ,那这就是一个符合条件的结果,添加到结果集中。
class Solution {
public:
vector<vector<int>> res;
vector<int> temp;
void backTracking(int sign, int n, int k) {
//终止条件
if( temp.size() == k){
res.push_back(temp);
return;
}
//递归
for(int i = sign; i <= n; i++) {
temp.push_back(i);
//因为不重复,所以下次递归就是从下一个位置开始的
backTracking(i + 1, n, k);
temp.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backTracking(1, n, k);
return res;
}
};
No 40. 组合总和 II(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-ii/
题目描述:
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
思路:这个题有一个特殊情况,那就是给定的数组中可能会有重复的元素,那么如果这个元素使用过了,后面再使用,那就会导致重复,但是我们又不能在一开始就删除掉重复的元素,否则就和给定的数组不一样了,结果肯定会不正确。所以我们只能在遍历的时候,一边遍历,一边进行去重的操作。
我的去重思路是这样:我们按照正常的递归回溯先操作,当递归返回的时候,将要进行下一次 FOR 循环 的时候,进行去重,如果下一个数字和这个使用过的数字是一样的,那就跳过,注意要是用while循环跳过,因为可能不止一个重复的。要想进行这样的操作必须首先对原数组进行排序,让相同的元素挨在一起。可是排序对原数组进行了修改,不会导致其它问题吗?这里我们找的是组合,和顺序没有关系,只要是这些数字,找到的就是这些组合,不会因为数组的顺序变化而导致组合的变化。
class Solution {
public:
//标准递归回溯
vector<vector<int>> res;
vector<int> res_temp;
void backTracking(vector<int>& nums, int target, int sum, int startIndex) {
//符合条件
if(sum == target) {
res.push_back(res_temp);
return;
}
//不符合条件
if(sum > target) {
return;
}
//递归
for(int i = startIndex; i < nums.size(); i++) {
sum += nums[i];
res_temp.push_back(nums[i]);
backTracking(nums, target, sum, i + 1);
res_temp.pop_back();
sum -= nums[i];
//关键步骤:当操作结束递归返回,准备开始下一轮时,如果发现当前数字和后面数字相同,那就跳过,否则会重复
//要用while,因为可能不止一个相同,但是这个要求nums有序;
while(i < nums.size() - 1 && nums[i] == nums[i + 1]) {
i++;
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
//这里排序是为了后面排除重复集合,让相同的元素放在一起
sort(candidates.begin(), candidates.end());
backTracking(candidates, target, 0, 0);
return res;
}
};
No 47. 全排列 II(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations-ii/
题目描述:
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
思路:这个题目求的是全排列,所以这时候就不在需要传递起始位置了,因为求全排列,数字是可以重复使用的,不同的顺序是不同的结果。但是题目给定的数组有重复的元素,应该如何去重?其实这里的去重思路和上面的题目一样,也是在使用过这个数字,将要进行下一次 for 循环的时候,看一下下一个数字是不是一样,一样的话就跳过,同样也需要对原数组进行排序。这里对原数组进行排序也没有影响,因为求的是全排列,只要是这些数字,那么最后得到的全排列一定是一样的。好了,原数组中重复的数字问题解决了,也就是同层有重复数字的问题解决了。(树层去重)
但是还有一个问题需要考虑。我们在递归的时候,先把当前拿到的数字添加到结果中,然后进行下一次递归,但是因为求全排列,下一次递归的起始位置也是 0 。那么可以想一下在第一次递归的时候,拿到的是下标为 0 的数字,下一次递归又是这个数字,而我们希望不要在使用现在使用过的这个数字了,这时候怎么办?这一点其实是在 纵深 方向的去重,解决方法是定义一个数组用来标定已经使用过的数字,如果这个数字使用过了,那就进行标记,下次递归的时候只有没有标记的数字才可以使用,当递归返回的时候,再取消标记。其实就是回溯。这样就可以避免下次递归的时候又用到了之前的递归使用过的数字。对树层去重还有一个方法就是在每次递归的 for 循环之前定义一个数组,用于标定同层使用过的数字。注意是要每次递归的 for 循环之前都要重新定义,这样才能保证和每一层对应。(树枝去重)
所以总结一下,在求排列的时候,不仅要对树层去重,还要对同一树枝去重。
class Solution {
public:
//优化:也可以对nums先排序,因为是求全排列,所以顺序无所谓,之后利用while循环来代替记录同层重复的容器即可
vector<vector<int>> res;
vector<int> res_temp;
void backTracking(vector<int>& nums, vector<int>& usedNum) {
if(res_