组合
题目描述
给定两个整数 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]]
解题思路
未优化版
递归流程说明:
1. 递归函数的参数和返回值
-
参数:
int startIndex
:当前递归开始的索引,用于控制组合中的数字不重复。int n
:可选数的上限。int k
:组合中的数字数量。
-
返回值:无返回值,直接修改类成员
result
来保存所有可能的组合。
2. 递归函数的终止条件
-
终止条件:
- 当
path.size() == k
时,说明已经找到了一个长度为k
的组合,此时将path
添加到result
中,并返回。
- 当
3. 递归函数的单层逻辑
-
单层逻辑:
- 循环从
startIndex
到n
并将当前索引的元素添加到path
中。 - 递归调用
backtracking(i + 1, n, k)
,其中i + 1
确保下一层递归不会选择当前或之前的元素,避免重复。 - 完成递归后,执行
path.pop_back()
进行回溯,这是回溯过程的关键,它确保了在返回上一层递归前,当前层的状态会被撤销,从而能够正确地探索其他可能的组合。
- 循环从
回溯过程说明:
在 backtracking
函数中,回溯发生在每次递归调用之后。当从递归深层返回到浅层(返回到循环中的下一次迭代)时,通过 path.pop_back()
撤销了之前做的选择(即移除了最后一个元素),这样就可以在接下来的循环迭代中尝试其他可能的数字,从而枚举所有可能的组合。这个过程持续进行,直到所有的路径都被探索完毕。
优化版
为了避免在n很大且k很大的时候,持续造成大量无用递归,可以针对上述为优化版本进行剪枝操作,将 i <= n
修改为
i <= n - (k - path.size()) + 1
即可,接下来解释这句代码:
具体逻辑:
k - path.size()
:这是当前还需要选择的数字的个数。n - i + 1
:这是从当前数字i
开始到数组末尾的数字总数。
为了确保剩余的位置可以被填满,我们需要 n - i + 1 >= k - path.size()
,简化这个不等式,我们得到:i <= n - k + path.size() + 1
代码实现
测试地址:https://leetcode.cn/problems/combinations/description/
未优化版:
class Solution {
private:
vector<int> path; // 用于保存当前的组合
vector<vector<int>> result; // 用于保存所有可能的组合
// 回溯函数
void backtracking(int startIndex, int n, int k) {
// 终止条件:当前组合的长度等于k
if (path.size() == k) {
result.push_back(path); // 将当前组合添加到结果集中
return;
}
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 将当前数字添加到组合中
backtracking(i + 1, n, k); // 递归调用,注意下一个开始的索引是i+1
path.pop_back(); // 回溯,移除上一步添加的数字
}
}
public:
// 主函数:生成从1到n的k个数的所有组合
vector<vector<int>> combine(int n, int k) {
backtracking(1, n, k); // 从数字1开始
return result;
}
};
优化版:
class Solution {
private:
vector<int> path; // 用于保存当前的组合
vector<vector<int>> result; // 用于保存所有可能的组合
// 回溯函数
void backtracking(int startIndex, int n, int k) {
// 终止条件:当前组合的长度等于k
if (path.size() == k) {
result.push_back(path); // 将当前组合添加到结果集中
return;
}
//剪枝
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
path.push_back(i); // 将当前数字添加到组合中
backtracking(i + 1, n, k); // 递归调用,注意下一个开始的索引是i+1
path.pop_back(); // 回溯,移除上一步添加的数字
}
}
public:
// 主函数:生成从1到n的k个数的所有组合
vector<vector<int>> combine(int n, int k) {
backtracking(1, n, k); // 从数字1开始
return result;
}
};
组合总和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,没有有效的组合。
解题思路
递归流程说明:
1. 递归函数的参数和返回值
-
参数:
int n
:目标和。int k
:组合中的数字数量。int startIndex
:当前递归开始的索引,用于控制组合中的数字不重复。int sum
:当前组合的和。
-
返回值:无返回值,直接修改类成员
result
来保存所有可能的组合。
2. 递归函数的终止条件
-
终止条件:
- 当
sum > n
时,说明当前和已经超过目标和,直接返回。 - 当
path.size() == k
时,说明已经找到了一个长度为k
的组合,此时检查和是否等于n
,如果等于则将path
添加到result
中,并返回。
- 当
3. 递归函数的单层逻辑
-
单层逻辑:
- 循环从
startIndex
到9 - (k - path.size()) + 1
并将当前索引的元素添加到path
中。 - 更新
sum
并递归调用backtracking(n, k, i + 1, sum)
,其中i + 1
确保下一层递归不会选择当前或之前的元素,避免重复。 - 完成递归后,执行
sum -= i
和path.pop_back()
进行回溯,这是回溯过程的关键,它确保了在返回上一层递归前,当前层的状态会被撤销,从而能够正确地探索其他可能的组合。
- 循环从
回溯过程及剪枝逻辑说明:
回溯发生在每次递归调用之后。当从递归深层返回到浅层(返回到循环中的下一次迭代)时,通过 sum -= i
和 path.pop_back()
撤销了之前做的选择(即移除了最后一个元素并更新了和),这样就可以在接下来的循环迭代中尝试其他可能的数字,从而枚举所有可能的组合。这个过程持续进行,直到所有的路径都被探索完毕。
剪枝逻辑:
- 在循环中,
i <= 9 - (k - path.size()) + 1
是一个剪枝条件,用于减少不必要的递归调用。这个条件确保了从当前数字i
开始到数组末尾的数字总数足够多,以填满剩余的位置。如果剩余的数字不足以填满这些位置,就没有继续递归的必要。 - 当
sum > n
时,说明当前组合的和sum
已经超过了目标和n
,继续递归下去显然不可能找到满足条件的组合,因此可以直接返回,不再进行后续的递归调用。
代码实现
测试地址:https://leetcode.cn/problems/combination-sum-iii/
class Solution {
private:
vector<int> path; // 用于保存当前的组合
vector<vector<int>> result; // 用于保存所有可能的组合
// 回溯函数
void backtracking(int n, int k, int startIndex, int sum) {
// 剪枝:如果当前和已经大于n,则直接返回
if (sum > n)
return;
// 终止条件:如果当前组合的长度等于k
if (path.size() == k) {
// 如果当前和等于n,则将当前组合添加到结果集中
if (sum == n) {
result.push_back(path);
}
return;
}
// 循环遍历可能的数字
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
sum += i; // 更新当前和
path.push_back(i); // 将当前数字添加到组合中
backtracking(n, k, i + 1, sum); // 递归调用,注意下一个开始的索引是i+1
sum -= i; // 回溯,移除上一步添加的数字并更新和
path.pop_back(); // 回溯,移除上一步添加的数字
}
}
public:
// 主函数:生成从1到9的k个数的所有组合,使得它们的和等于n
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n, k, 1, 0); // 从数字1开始,初始和为0
return result;
}
};
电话号码的字母组合
题目描述
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
解题思路
递归流程说明:
1. 递归函数的参数和返回值
-
参数:
const string &digits
:输入的数字字符串。int index
:当前处理的数字在字符串中的索引。
-
返回值:无返回值,直接修改类成员
result
来保存所有可能的组合。
2. 递归函数的终止条件
-
终止条件:
- 当
index == digits.size()
时,说明已经处理完所有数字,将当前组合path
添加到结果集result
中,并返回。
- 当
3. 递归函数的单层逻辑
-
单层逻辑:
- 获取当前数字
digit
对应的字母字符串letters
。 - 循环遍历
letters
中的每个字母,将当前字母添加到path
中。 - 递归调用
backtracking(digits, index + 1)
,处理下一个数字。 - 完成递归后,执行
path.pop_back()
进行回溯,移除上一步添加的字母,以便尝试下一个字母。
- 获取当前数字
回溯过程:
- 在每次递归调用之后,通过
path.pop_back()
撤销之前的选择(即移除了最后一个字母),这样就可以在接下来的循环迭代中尝试其他可能的字母,从而枚举所有可能的组合。
代码实现
测试地址:https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
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
};
string path; // 用于保存当前的组合
vector<string> result; // 用于保存所有可能的组合
// 回溯函数
void backtracking(const string &digits, int index) {
// 终止条件:如果当前索引等于输入数字的长度
if (index == digits.size()) {
result.push_back(path); // 将当前组合添加到结果集中
return;
}
int digit = digits[index] - '0'; // 获取当前数字
string letters = letterMap[digit]; // 获取当前数字对应的字母
for (int i = 0; i < letters.size(); i++) {
path.push_back(letters[i]); // 将当前字母添加到组合中
backtracking(digits, index + 1); // 递归调用,处理下一个数字
path.pop_back(); // 回溯,移除上一步添加的字母
}
}
public:
// 主函数:生成数字对应的字母组合
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) {
return result; // 如果输入为空,直接返回空结果
}
backtracking(digits, 0); // 从第一个数字开始回溯
return result;
}
};