回溯法可以抽象成树形结构
集合的大小构成了树的宽度,递归的深度构成的树的深度。
回溯法是在递归的基础上 加上for循环 在树形结构中for循环是为了横向遍历 递归是纵向遍
回溯模板
返回值一般为void
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
if是遍历到叶子节点 找到其中的一个结果 存入 并结束本层递归
for循环是横向遍历 if终止条件是为了控制for循环几次
回溯撤回本次结果中的一个 然后方便下一个元素进入
第77题. 组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
从1-n中选结果 每个结果的大小为k
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear(); // 可以不写
path.clear(); // 可以不写
backtracking(n, k, 1);
return result;
}
};
终止条件:当path的大小等于k时说明到叶子节点了,返回本次结果
终止条件的意义时为了控制for循环几次 当k=2时 每个结果集是两个数 所以控制到两层for循环
srartindex是为了防止数字重复
pop_back() 回溯 【1,2】到达叶子节点 收集结果 pop 掉 2 然后 【1,3】
查找过程:
优化:
红色数值没有意义
优化后:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
[x,n]
n-x+1 代表这段x到n的长度
n-x+1> = k - path.size() 满足k个还需要找的元素
整理出来就是优化后的结果
216.组合总和III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
public:
void dfs(int k,int n,int index,int sum){
if(path.size()==k){
if(sum==n){
result.push_back(path);
return;
}
}
for(int i=index;i<=9;i++){
path.push_back(i);
sum+=i;
dfs(k,n,i+1,sum);
sum-=i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k,n,1,0);
return result;
}
};
与上一题类似 终止条件是遇到叶子节点 看sum与目标和相等不相等
for循环存入i 与相加的和 并回溯掉相加的和 与i
17.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
class Solution {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
public:
vector<string> result;
string s;
void backtracking(const string& digits, int index) {
if (index == digits.size()) {
result.push_back(s);
return;
}
int digit = digits[index] - '0'; // 将index指向的数字转为int
string letters = letterMap[digit]; // 取数字对应的字符集
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]); // 处理
backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
}
vector<string> letterCombinations(string digits) {
s.clear();
result.clear();
if (digits.size() == 0) {
return result;
}
backtracking(digits, 0);
return result;
}
};
先用string数组将键盘数字对应的字母存入 数组下标代表数字 从0开始
index记录遍历到第几个数字了
index等于输入数字的个数 返回其中的一个结果
digits是string数组 - ‘0’ 才能用int存入
string letters将数字对应的字母存入
for循环遍历数字键对应的字母
index+1 下一层递递归处理的是下一个数字
39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
套用模板公式即可,终止条件和大于目标值直接返回,和等于目标值存入数组,i<的是树的头节点有几个元素
因为本题元素为可重复选取的所以递归那传i
就是for循环条件和递归条件那需要斟酌一下,其他大差不差。
// 版本一
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtracking(candidates, target, 0, 0);
return result;
}
};
40.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
path.clear();
result.clear();
// 首先把给candidates排序,让其相同的元素都挨在一起。
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
集合(数组candidates)有重复元素,但还不能有重复的组合
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
强调一下,树层去重的话,需要对数组排序!
sum + candidates[i] <= target为剪枝操作
排序
used bool数组
数层去重