77. 组合
题目描述:
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
解答:
如果按照暴力来想,直接for循环,但是这样对于k较小的是可以,但是如果k为100,总不能写100层的for循环吧,那也太离谱了。
所以还是应该想想其他办法。
组合问题——回溯!
在做二叉树的时候222. 完全二叉树的节点个数、110. 平衡二叉树、257.二叉树的所有路径_清榎的博客-CSDN博客t
提到过回溯。回溯和递归是一体的,属于是那种你中有我,我中有你的关系。
回溯。回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
所以其实回溯法并不高效,但是遇到一些问题,没办法,除了暴力就是回溯了,最多是 回溯+剪枝。
先插个眼,回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
回溯可以把问题抽象成一棵树,树的宽度和深度都有限,其中深度也就是递归的层数,宽度就是问题的大小。
表现在编程时也就是深度使用递归,宽度使用for循环,因此 回溯的一般写法为:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
接下来就正式解题了,
为了思路清晰仿照递归三部曲也整一个回溯三部曲:
(1)参数及返回值:
n,k肯定需要当作参数,还有下次从哪开始找也需要记录
返回值可以不需要,直接开全局变量记录即可 。
(2)终止条件:
树搜索到叶子就可以终止了,叶子是什么情况呢?
满足k个数——即为path中数量为k
(3)内部处理逻辑:
先放入当前访问的节点,再递归,递归完成后进行回溯。
代码实现:
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backTrace(int n, int k, int start){
if (path.size() == k){
result.push_back(path);
return;
}
for (int i = start; i <= n; i++){
path.push_back(i);
backTrace(n, k, i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backTrace(n, k, 1);
return result;
}
};
剪枝优化:
前面提到回溯往往可以进行剪枝,这道题也可以。
比如(4,4)这种情况。
后面的数根本没四个了,还判断啥,没必要了,直接剪枝。
所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了
修改for循环条件:n - (k - path.size()) + 1
216. 组合总和 III
题目描述:
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
解答:
这道题和上道题十分相似,不一样的是把树的宽度给隐藏了
很清楚,树的深度为k,那宽度应该取多少?
数字1-9,所以宽度为9
这样一来就简单了,每次取一个值放入path中,一直取到path中个数等于k为止。
再判断是否等于n,等于就放入result里。
然后返回,回溯 ,继续往下寻找即可。
回溯三部曲:
(1)参数及返回值:
n,k肯定需要当作参数,还有下次从哪开始找也需要记录
返回值可以不需要,直接开全局变量记录即可 。
(2)终止条件:
树搜索到叶子就可以终止了,叶子是什么情况呢?
满足k个数——即为path中数量为k
(3)内部处理逻辑:
先放入当前访问的节点,再递归,递归完成后进行回溯。
代码实现:
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backTrace(int n, int k, int start){
if (path.size() == k){
int sum = 0;
for (int i = 0; i < k; i++){
sum += path[i];
}
if (sum == n)
result.push_back(path);
return;
}
for (int i = start; i <= 9; i++){
path.push_back(i);
backTrace(n, k, i + 1);
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backTrace(n, k, 1);
return result;
}
};
剪枝优化:
比较容易想到的是,如果我们当前节点的sum和已经大于等于n,那就没有继续往下寻找的必要了,直接剪枝。
这种需要将sum作为参数进行传递,同时回溯时也需要相应对sum进行修改。
代码实现:
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backTrace(int n, int k, int start, int sum){
//剪枝操作
if (sum > n)
return;
if (path.size() == k){
if (sum == n)
result.push_back(path);
return;
}
for (int i = start; i <= 9; i++){
path.push_back(i);
sum += i;
backTrace(n, k, i + 1, sum);
sum -= i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backTrace(n, k, 1, 0);
return result;
}
};
当然也可以和上一题一样,对for循环进行剪枝,条件改为
9 - (k - path.size()) + 1
即可。
17. 电话号码的字母组合
题目描述:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解答:
这个问题本质上和前两题还是类似。
需要考虑三个问题:
数字和字母如何映射?
树的宽度?
树的深度?
字母和数字的映射可以采用map或者数组,如下所示:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
至于树的宽度深度,我们拿到一串数字,数字的长度其实就是树的深度;每一个数字都会对应三个或者四个字母,这便是树的宽度。如下图所示
考虑清楚这三个问题就可以开始编写代码了。
回溯三要素:
(1)参数和返回值:
首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量依然定义为全局。
再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index,标识从哪个数字开始。
(2)终止条件:长度等于输入的数字长度,记录进结果中。
(3)内部处理逻辑:拿到数字先获取到其对应的字母,然后进入循环,递归。
int digit = digits[index] - '0'; // 将index指向的数字转为int
string letters = letterMap[digit]; // 取数字对应的字符集
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]); // 处理
backTrace(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
代码实现:
class Solution {
public:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string s;
void backTrace(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]); // 处理
backTrace(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
}
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) {
return result;
}
backTrace(digits, 0);
return result;
}
};