216.组合总和 III
216. 组合总和 III - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-iii/
题目描述:
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
思路分析:
这道题是在77.组合的基础上做了改进,相关回溯解法可以参考上一篇博文:
算法练习第20天|回溯算法 77.组合问题 257. 二叉树的所有路径-CSDN博客
和77组合题目一样,本题抽象成二叉树的逻辑如下(图片来自卡哥的代码随想录):
使用回溯的解法,我们依然要使用path和result两个vector,一个用于记录遍历路径,另一个用于记录满足条件的结果:
vector<vector<int>> result; //满足条件的结果
vector<int> path; //当前路径
下面我们回顾一下上一篇博文中所讲的回溯三部曲:
第一步:确认回溯函数的参数和返回值。
void backtracking(参数)
{
}
一般来说,回溯函数的返回值类型为void,至于参数,为了表达方便,我们定义了目标和targetSum(即题目中的n)、k、遍历到当前路径的和sum、以及每一层回溯的开始索引startIndex。具体代码如下:
// int targetSum; //目标和
// int k; //k个元素
// int sum; //当前path记录的元素的和
// int startIndex; //开始的索引
//回溯第一步:确认回溯函数的参数以及返回值
void backTracking(int targetSum, int k, int sum, int startIndex)
{
}
第二步:确认回溯的终止条件。
if (终止条件) {
存放结果;
return;
}
什么时候本次回溯终止?那就是我们成功的找到了一组数据,里面有k个元素,且它们的和为n。那么终止条件就是 path.size() == k && sum == targetSum。找到这一组数据则需要将其记录在结果result中,然后return。具体代码如下:
if(path.size() == k && sum == targetSum)
{
result.push_back(path);
return;
}
但其实更严谨的逻辑应该是, 先检查是否遍历了k个元素,即path的size是否为k。如果遍历了k个元素,则判断它们的和sum是否等于targetSum,如果相等,则记录该组数据;不相等则不记录。但是不论是否相等,此时已经是遍历了k个元素,已经不能再继续遍历了,所以直接return,结束掉本次的回溯。代码如下:
if(path.size() == k )
{
if(sum == targetSum)
result.push_back(path);
return;
}
如果不满足上述的第一个条件,则说明还没有遍历到k个元素,应该执行单层回溯的具体逻辑。
第三步:确认单层回溯的遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
这一过程要做哪些事情?
首先,从startIndex开始遍历元素,将当前元素加到sum中,并用path加以记录;然后就可以从startIndex+1的位置进行递归了,递归之后,该记录的结果会进行记录。然后要进行抽象成的二叉树遍历过程回溯过程,即当前节点退出sum和path,具体代码如下:
for(int i = startIndex; i<=9; i++)
{
//处理节点
sum += i;
path.push_back(i);
//递归
backTracking(targetSum, k, sum, i+1); //注意将i+1调整为startIndex
//回溯
sum -= i;
path.pop_back(i);
}
完整代码:
class Solution {
public:
vector<vector<int>> result; //满足条件的结果
vector<int> path; //当前路径
// int targetSum; //目标和
// int k; //k个元素
// int sum; //当前path记录的元素的和
// int startIndex; //开始的索引
//回溯第一步:确认回溯函数的参数以及返回值
void backTracking(int targetSum, int k, int sum, int startIndex)
{
//回溯第二步:确认回溯函数的终止条件
//什么时候终止此次回溯?那就是找到了一组数(k个数),且它们的和为n
if(path.size() == k )
{
if(sum == targetSum)
result.push_back(path);
return;
}
//回溯第三步:确认单层回溯的遍历过程
//单层回溯要做那些事情?遍历!从startIndex开始遍历
for(int i = startIndex; i<=9; i++)
{
//处理节点
sum += i;
path.push_back(i);
//递归
backTracking(targetSum, k, sum, i+1); //注意将i+1调整为startIndex
//回溯
sum -= i;
path.pop_back(i);
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backTracking(n,k,0,1);
return result;
}
};
进一步剪枝处理优化代码:
剪枝其实就是在二叉树遍历逻辑的基础上,去点一些明显没有必要的遍历路径,如下图所示:
当遍历元素的和大于题目要求的n时,其实已经没有意义了。
那么剪枝的地方可以放在递归函数开始的地方,剪枝代码如下:
if (sum > targetSum) { // 剪枝操作
return;
}
除此之外,遍历过程也可以再剪枝:
接下来看一下优化过程如下:
-
已经选择的元素个数:path.size();
-
所需需要的元素个数为: k - path.size();
-
列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
-
在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历,往后找还能找到k个数。
代码如下:
class Solution {
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (sum > targetSum) { // 剪枝操作
return;
}
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
}
};
17.电话号码的字母组合
17. 电话号码的字母组合 - 力扣(LeetCode)https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/题目描述:
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "" 输出:[]
示例 3:
输入:digits = "2" 输出:["a","b","c"]
数字和字母如何映射
可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下:
const string letterMap[10] = {
"", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz", //9
};
回溯法来解决n个for循环的问题
例如:输入:"23",抽象为树形结构,如图所示:
这就和之前的77组合以及216组合III有了相似之处。然后只用回溯三部曲。
回溯三部曲:
- 确定回溯函数参数
首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。
再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。这个index用来表示遍历到digits中的第几个数字了。与之前的startIndex不一样。
vector<string> result;
string path;
//回溯第一步
void backTracking(const string &digits, int index)
{
}
- 确认回溯的终止条件
index表示遍历到了digits的第几个数字,其初始值为0,遍历过一个数字后就会+1,那么当index等于digits.size()时,表明遍历完毕。代码如下:
if (index == digits.size()) {
result.push_back(s);
return;
}
- 确认单层回溯逻辑
首先,要根据index把数字对应的字符串取出来,然后遍历该字符串,将字母加入到路径path中。添加完一个字母(对应模板中的处理节点),则 该去执行递归,index+1,然后回溯:
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(); // 回溯
}
整体代码如下:
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 path;
//回溯第一步
void backTracking(const string digits, int index)
{
//回溯第二步:确认回溯终止条件
if(index == digits.size())
{
result.push_back(path);
return ;
}
//回溯第三步:确认单层回溯逻辑操作
int num = digits[index] - '0'; //获取对应的数字
string letters = letterMap[num]; //获取该数字对应的字母
for(int i = 0 ; i < letters.size(); i++)
{
path.push_back(letters[i]); //加入一个字母
backTracking(digits, index+1); //找下一个数字对应的字母
path.pop_back(); //回溯
}
}
vector<string> letterCombinations(string digits) {
if(digits.empty())
return result;
backTracking(digits, 0);
return result;
}
};