回溯
解决一个回溯问题,实际上就是一个决
策树的遍历过程。你只需要思考3个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件
框架
result = []
def backtrack(路径,选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表 //状态重置
举例说明
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
class Solution {
public:
void backtrack(int l,vector<vector<int>>& res,vector<int> candidates,int target,vector<int> track) {
if(0==target) {
res.push_back(track);
return;
} else if(target<0) {
return;
}
for(int i=l;i<candidates.size();i++) { //注意这里是L
if(target-candidates[i]<0)
break; //剪枝
track.push_back(candidates[i]);
target-=candidates[i];
backtrack(i,res,candidates,target,track); //排除重复选项
track.pop_back(); // 状态重置
target+=candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> track;
int l=0;
backtrack(l,res,candidates,target,track);
return res;
}
};
这道题目的精华就是去除重复项,设置搜索起点begin
画出树形图
以输入:candidates = [2, 3, 6, 7]
, target = 7
为例:
说明:
以 target = 7 为 根结点 ,创建一个分支的时 做减法 ;
每一个箭头表示:从父亲结点的数值减去边上的数值,得到孩子结点的数值。边的值就是题目中给出的 candidate 数组的每个元素的值;
减到 00 或者负数的时候停止,即:结点 00 和负数结点成为叶子结点;
所有从根结点到结点 00 的路径(只能从上往下,没有回路)就是题目要找的一个结果。
这棵树有 44 个叶子结点的值 00,对应的路径列表是 [[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]],而示例中给出的输出只有 [[7], [2, 2, 3]]。即:题目中要求每一个符合要求的解是 不计算顺序 的。下面我们分析为什么会产生重复。
针对具体例子分析重复路径产生的原因(难点)
友情提示:这一部分我的描述是晦涩难懂的,建议大家先自己观察出现重复的原因,进而思考如何解决。
产生重复的原因是:在每一个结点,做减法,展开分支的时候,由于题目中说 每一个元素可以重复使用,我们考虑了所有的候选数,因此出现了重复的列表。
一种简单的去重方案是借助哈希表的天然去重的功能,但实际操作一下,就会发现并没有那么容易。
可不可以在搜索的时候就去重呢?答案是可以的。遇到这一类相同元素不计算顺序的问题,我们在搜索的时候就需要 按某种顺序搜索。具体的做法是:每一次搜索的时候设置 下一轮搜索的起点 begin,请看下图。
即:从每一层的第 2 个结点开始,都不能再搜索产生同一层结点已经使用过的 candidate
里的元素。
什么时候使用 used 数组,什么时候使用 begin 变量
什么时候使用 used 数组,什么时候使用 begin 变量
有些朋友可能会疑惑什么时候使用 used 数组,什么时候使用 begin 变量。这里为大家简单总结一下:
排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。
注意:具体问题应该具体分析, 理解算法的设计思想 是至关重要的,请不要死记硬背。
另外一个题目216
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
答案
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> combinationSum3(int k, int n) {
int sum=0;
backtrak(1,k,n,sum);
return res;
}
void backtrak(int i,int k,int n,int sum) {
if(sum==n&&track.size()==k) {
res.push_back(track);
}
for(i;i<10;i++) {
if(track.size()>k||sum>n){
return;
}
auto a=find(track.begin(),track.end(),i);
if(a!=track.end()) {
continue;
} else {
sum+=i;
track.push_back(i);
}
backtrak(i,k,n,sum);
int b=track.back();
track.pop_back();
sum-=b;
}
}
};
另外一个题目
377. 组合总和 Ⅳ
难度中等202
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
超时了
class Solution {
vector<vector<int>> used;
vector<int> track;
int res=0;
public:
int combinationSum4(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
back_track(nums,target);
return res;
}
void back_track(vector<int>& nums, int target) {
if(target==0) {
for(auto a:track) {
cout<<a;
}
cout<<endl;
auto result=find(used.begin(),used.end(),track);
if(result !=used.end()) {
return;
}
res++;
used.push_back(track);
return;
}
for(int i=0;i<nums.size();i++) {
if(target-nums[i]<0) return;
track.push_back(nums[i]);
target-=nums[i];
back_track(nums,target);
target+=nums[i];
track.pop_back();
}
}
};