题目描述
77. 组合(力扣中等难度)
给定两个整数 'n' 和 'k',返回范围 '[1, n]' 中所有可能的 'k' 个数的组合。可以按任何顺序返回答案。
解答代码
class Solution {
public:
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();
}
}
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};
代码逻辑
1. 定义两个成员变量:'result' 用于存放所有符合条件的组合结果,'path' 用于存放当前正在构造的组合路径。
2. 在 'combine' 函数中初始化 'result' 和 'path',然后调用 'backtracking' 函数开始搜索。
3. 'backtracking' 函数的具体流程如下:
- 如果 'path' 的长度等于 'k',则说明找到了一个满足条件的组合,将 'path' 加入到 'result' 中。
- 否则,从 'startIndex' 开始到 'n' 遍历每个数字 'i'。
- 将当前数字 'i' 加入到 'path' 中,递归调用 'backtracking' 函数,并将起始位置 'startIndex' 更新为 'i + 1' 以避免重复数字。
- 回溯操作:从递归返回后,将当前加入的数字 'i' 从 'path' 中移除,以便尝试新的组合路径。
代码难点
1. 回溯与路径管理:回溯的关键在于通过路径添加和移除操作来维护当前组合状态。每次在 'path' 中添加一个新数字后,需要递归处理,递归完成后再回退一步,以保证 'path' 状态正确。
2. 避免重复:在 'backtracking' 函数中,'for' 循环起始于 'startIndex',通过递增 'i' 保证每次组合中的元素不重复,也确保了不再包含之前的数字,避免了不必要的重复组合。
3. 时间复杂度:组合问题的复杂度相对较高,因为每个数字组合都需要遍历和构造。
题目描述:
216. 组合总和 III(力扣中等难度)
找出所有相加之和为 'n' 的 'k' 个数的组合,且满足以下条件:
1. 只使用数字 1 到 9
2. 每个数字最多使用一次
返回所有可能的有效组合的列表。列表中的组合可以按任意顺序返回,且不能包含重复组合。
解答代码:
class Solution {
public:
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;
}
}
// 剪枝
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
sum += i;
path.push_back(i);
backtracking(targetSum, k, sum, i + 1);
// 回溯
sum -= i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
}
};
代码逻辑:
1. 在 'combinationSum3' 函数中初始化 'result' 和 'path',然后调用 'backtracking' 函数开始搜索组合
2. 'backtracking' 函数的主要流程如下:
- 首先判断当前和 'sum' 是否大于目标和 'targetSum',如果是则立即返回(剪枝)
- 如果当前组合的长度等于 'k',则进一步判断 'sum' 是否等于 'targetSum',如果相等,将当前组合 'path' 添加到 'result' 中
- 否则,从 'startIndex' 开始到 '9 - (k - path.size()) + 1' 遍历每个数字 'i'。'9 - (k - path.size()) + 1' 是一种剪枝方式,确保剩余的数字数量足够完成组合
- 将当前数字 'i' 加入 'path',递归调用 'backtracking' 函数继续构建组合
- 递归返回后进行回溯,将当前数字从 'sum' 和 'path' 中移除,尝试其他可能的组合路径
代码难点:
1. 剪枝:在 'for' 循环中,通过 '9 - (k - path.size()) + 1' 来限制循环范围,可以有效减少不必要的搜索
2. 条件判断顺序:先判断 'sum > targetSum' 可以避免不必要的递归
3. 回溯:在递归中每次加入新的数字 'i' 后,需要在返回时将其移出,以保证路径状态
题目描述:
17. 电话号码的字母组合(力扣中等难度)
给定一个仅包含数字 '2-9' 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。数字到字母的映射如下(与电话按键相同):
解答代码:
class Solution {
public:
vector<string> result;
string s;
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
void backtracking(const string& digits, int index) {
if (index == digits.size()) {
result.push_back(s);
return;
}
int digit = digits[index] - '0';
string letters = letterMap[digit];
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]);
// 为什么是index + 1
backtracking(digits, 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;
}
};
代码逻辑:
1. 'letterCombinations' 函数用于初始化结果和中间字符串 's',并在 'digits' 为空时直接返回空结果
2. 'backtracking' 函数主要负责递归生成所有字母组合路径
- 当 'index' 等于 'digits' 长度时,说明已生成一条完整的字母组合,将当前字符串 's' 加入 'result'
- 否则,找到当前数字对应的字母字符串 'letters',并通过 'for' 循环依次添加每个字母到 's'
- 对每个字母递归调用 'backtracking',并在递归返回后将 's' 中的最后一个字母移除(回溯)
代码难点:
1. index + 1 的使用:每次递归时通过 'index + 1' 来处理下一个数字,以确保每个位置都能对应正确的字母组合
2. 递归与回溯:递归过程中 's.push_back()' 和 's.pop_back()' 的配合确保了路径正确
题目描述:
39. 组合总和(力扣中等难度)
给你一个无重复元素的整数数组 'candidates' 和一个目标整数 'target',找出 'candidates' 中所有和为 'target' 的不同组合,并以列表形式返回。'candidates' 中的同一个数字可以无限次重复被选取。如果至少有一个数字的使用次数不同,则视为不同的组合。
解答代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int sum, int target, int startIndex) {
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {
path.push_back(candidates[i]);
sum += candidates[i];
backtracking(candidates, sum, target, i);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtracking(candidates, 0, target, 0);
return result;
}
};
代码逻辑:
1. 'combinationSum' 函数首先清空 'result' 和 'path',然后调用 'backtracking' 函数开始搜索
2. 'backtracking' 函数的流程如下:
- 若当前和 'sum' 超过 'target',直接返回(剪枝操作,减少不必要的递归)
- 若当前和 'sum' 等于 'target',将 'path' 作为有效组合添加到 'result'
- 否则,从 'startIndex' 开始遍历 'candidates' 数组
- 将 'candidates[i]' 加入 'path' 并更新 'sum'
- 递归调用 'backtracking',传入当前索引 'i' 以允许重复选择当前数字
- 递归返回后执行回溯操作,移除 'path' 的最后一个元素并更新 'sum'
代码难点:
1. 允许重复选择:递归调用时将当前索引 'i' 传入 'backtracking',使得后续递归仍能选择当前数字,从而实现重复选择
2. 剪枝:当 'sum' 大于 'target' 时立即返回,避免不必要的计算
题目描述:
40. 组合总和 II(力扣中等难度)
给定一个候选人编号的集合 `candidates` 和一个目标数 `target`,找出 `candidates` 中所有和为 `target` 的组合。`candidates` 中的每个数字在每个组合中只能使用一次。
解答代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int sum, int target, int startIndex, vector<bool>& used) {
if (sum > target) return;
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {
if (i > 0 && candidates[i - 1] == candidates[i] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, sum, target, i + 1, used);
// 回溯
sum -= candidates[i];
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(), false);
result.clear();
path.clear();
backtracking(candidates, 0, target, 0, used);
return result;
}
};
代码逻辑:
1. 在 `combinationSum2` 函数中,首先对 `candidates` 数组进行排序,以便处理重复数字,并初始化 `used` 数组、`result` 和 `path`
2. `backtracking` 函数的流程如下:
- 如果当前和 `sum` 超过 `target`,立即返回
- 如果当前和 `sum` 等于 `target`,将 `path` 作为有效组合添加到 `result`
- 否则,从 `startIndex` 开始遍历 `candidates` 数组
- 为了避免重复组合,如果 `candidates[i]` 和前一个数字相同,并且前一个数字未被使用,则跳过该数字
- 将 `candidates[i]` 加入 `path` 并更新 `sum`,同时将 `used[i]` 标记为 `true`
- 递归调用 `backtracking`,传入 `i + 1` 以确保每个数字只能使用一次
- 递归返回后执行回溯操作,移除 `path` 中的最后一个元素并更新 `sum`,同时将 `used[i]` 重置为 `false`
代码难点:
1. 去重处理:排序后通过判断 `candidates[i]` 是否等于前一个数字,并检查前一个数字是否已被使用,避免了相同数字在同一层重复使用,确保组合唯一
2. 递归与回溯:使用 `used` 数组跟踪元素使用情况,配合回溯操作确保组合路径状态
题目描述:
131. 分割回文串(力扣中等难度)
给定一个字符串 's',请将 's' 分割成一些子串,使每个子串都是回文串,返回所有可能的分割方案。
解答代码:
class Solution {
public:
vector<vector<string>> result;
vector<string> path;
void backtracking(const string& s, int startIndex) {
if (startIndex == s.size()) {
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i))
{
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
}
else {
continue;
}
backtracking(s, i + 1);
path.pop_back();
}
}
bool isPalindrome(const string& s, int start, int end) {
while(start <= end) {
if (s[start] != s[end]) {
return false;
}
start++;
end--;
}
return true;
}
vector<vector<string>> partition(string s) {
result.clear();
path.clear();
backtracking(s, 0);
return result;
}
};
代码逻辑:
1. 'partition' 函数初始化 'result' 和 'path',然后调用 'backtracking' 函数开始搜索
2. 'backtracking' 函数的流程如下:
- 如果 'startIndex' 等于字符串 's' 的长度,说明已到达字符串末尾,将 'path' 作为一个分割方案添加到 'result'
- 否则,从 'startIndex' 到字符串末尾遍历子串
- 对于每个子串,若 'isPalindrome' 判断为回文,则将该子串添加到 'path'
- 递归调用 'backtracking',起始位置设为 'i + 1',尝试进一步分割
- 递归返回后进行回溯操作,移除 'path' 中的最后一个子串,恢复当前路径状态
代码难点:
1. 判断回文子串:通过 'isPalindrome' 函数判断 's[start]' 到 's[end]' 是否为回文,保证每个分割的子串都是回文
2. 递归与回溯:'path.push_back' 和 'path.pop_back' 操作确保在每个递归层中保持路径的正确性
题目描述:
93. 复原 IP 地址(力扣中等难度)
有效 IP 地址由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0)构成,整数之间用 '.' 分隔。给定一个只包含数字的字符串 `s`,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 `s` 中插入 '.' 来形成。你不能重新排序或删除 `s` 中的任何数字。你可以按任意顺序返回答案。
解答代码:
class Solution {
public:
vector<string> result;
void backtracking(string& s, int startIndex, int pointNum) {
if (pointNum == 3) {
// 判断第四个字串
if (isValid(s, startIndex, s.size() - 1)) {
result.push_back(s);
}
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isValid(s, startIndex, i)) {
s.insert(s.begin() + i + 1, '.');
pointNum++;
backtracking(s, i + 2, pointNum);
// 回溯
pointNum--;
s.erase(s.begin() + i + 1);
}
}
}
bool isValid(const string& s, int start, int end) {
if (start > end) {
return false;
}
if (s[start] == '0' && start != end) { // 0开头的不合法
return false;
}
int num = 0;
for (int i = start; i <= end ; i++) {
if (s[i] > '9' || s[i] < '0') {
return false;
}
num = num * 10 + (s[i] - '0');
if (num > 255) {
return false;
}
}
return true;
}
vector<string> restoreIpAddresses(string s) {
result.clear();
if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了
backtracking(s, 0, 0);
return result;
}
};
代码逻辑:
1. `restoreIpAddresses` 函数会首先检查字符串 `s` 的长度是否在 4 到 12 之间。如果不满足条件,则返回空结果(剪枝)。
2. `backtracking` 函数的流程如下:
- 若 `pointNum` 为 3,表示已经插入了 3 个点,检查剩余部分是否为一个有效的 IP 地址段。如果有效,加入结果 `result`。
- 否则,从 `startIndex` 开始遍历每个子串,检查子串是否符合 IP 地址段的有效性(使用 `isValid` 函数)。
- 如果有效,插入一个点,然后递归调用 `backtracking` 继续处理后面的部分。
- 回溯时移除插入的点,恢复状态。
3. `isValid` 函数检查字符串是否符合一个有效的 IP 地址段:
- 若字符串的第一个字符为 `0` 且长度大于 1,则无效(不能有前导零)。
- 若转换为整数后大于 255,则无效。
- 如果没有上述问题,则返回有效。
代码难点:
1. 回溯与回溯路径管理:通过递归和回溯插入 `.`,并在回溯时恢复状态,确保不会重复计算。
2. IP 地址的合法性验证:需要判断子串是否满足 IP 地址段的有效性(范围 0-255,不能有前导零)。
题目描述:
78. 子集(力扣中等难度)
给定一个整数数组 `nums`,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集,你可以按任意顺序返回解集。
解答代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
if (startIndex >= nums.size()) {
return;
}
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
代码逻辑:
1. 在 `subsets` 函数中,初始化结果 `result` 和路径 `path`,然后调用 `backtracking` 函数进行递归搜索。
2. `backtracking` 函数:
- 每次递归都会将当前的路径 `path` 加入到结果 `result` 中。
- 通过 `startIndex` 来控制递归的起始位置,确保不会重复包含子集中的元素。
- 在每一层递归中,遍历当前索引 `startIndex` 到数组末尾的元素,将元素加入路径 `path` 中,并递归进入下一层。
- 回溯时,使用 `path.pop_back()` 恢复路径状态,保证遍历到每个可能的子集。
3. 最终,`result` 中保存了所有可能的子集。
代码难点:
1. 回溯的深度控制:通过 `startIndex` 确保每个元素在子集中只能出现一次,避免重复的子集。
2. 子集的生成:每一次递归都生成一个新的子集并加入结果中,这保证了幂集中的所有可能子集被遍历到。
题目描述:
78. 子集(力扣中等难度)
给定一个整数数组 `nums`,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集,你可以按任意顺序返回解集。
解答代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool>& used, int startIndex) {
result.push_back(path);
if (startIndex >= nums.size()) {
return;
}
for (int i = startIndex; i < nums.size(); i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 而我们要对同一树层使用过的元素进行跳过
if (i > 0 && nums[i - 1] == nums[i] && used[i - 1] == false) continue;
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, used, i + 1);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(), false);
// 排序
sort(nums.begin(), nums.end());
backtracking(nums, used, 0);
return result;
}
};
代码逻辑:
1. 在 `subsets` 函数中,初始化结果 `result` 和路径 `path`,然后调用 `backtracking` 函数进行递归搜索。
2. `backtracking` 函数:
- 每次递归都会将当前的路径 `path` 加入到结果 `result` 中。
- 通过 `startIndex` 来控制递归的起始位置,确保不会重复包含子集中的元素。
- 在每一层递归中,遍历当前索引 `startIndex` 到数组末尾的元素,将元素加入路径 `path` 中,并递归进入下一层。
- 回溯时,使用 `path.pop_back()` 恢复路径状态,保证遍历到每个可能的子集。
3. 最终,`result` 中保存了所有可能的子集。
代码难点:
1. 回溯的深度控制:通过 `startIndex` 确保每个元素在子集中只能出现一次,避免重复的子集。
2. 子集的生成:每一次递归都生成一个新的子集并加入结果中,这保证了幂集中的所有可能子集被遍历到。
题目描述:
90. 子集 II(力扣中等难度)
给定一个整数数组 `nums`,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。返回的解集中,子集可以按任意顺序排列。
解答代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() > 1)
result.push_back(path);
// uset的作用是对本层去重
unordered_set<int> uset;
for (int i = startIndex; i < nums.size(); i++) {
// ?
if ((!path.empty() && nums[i] < path.back()) ||
uset.find(nums[i]) != uset.end()) {
continue;
}
// 数层去重,每次递归都会定义新的uset,故不需要回溯
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
```
代码逻辑:
1. `subsetsWithDup` 函数会首先对输入数组 `nums` 进行排序。排序是为了方便跳过重复元素(确保同一层树中不会重复选取相同的元素)。
2. `backtracking` 函数:
- 每次递归都会将当前的路径 `path` 加入到结果 `result` 中。
- 对于每个元素,在递归时检查是否可以使用该元素:
- 如果当前元素 `nums[i]` 与前一个元素相同,并且前一个元素在同一树层已被跳过,则跳过当前元素,以避免重复子集。
- 若当前元素可以使用,将其加入路径 `path`,并递归处理下一部分。
- 回溯时移除路径中的最后一个元素,并恢复元素的使用状态 `used[i]`。
3. 最终,`result` 中保存了所有不重复的子集。
代码难点:
1. 跳过重复元素:通过排序和 `used` 数组来确保在同一树层中不会选择重复的元素,避免生成重复的子集。
2. 回溯与状态恢复:使用 `path` 记录当前的子集,利用 `used` 数组进行元素的使用状态控制,确保在递归时不会重复选择相同的元素。
题目描述:
46. 全排列(力扣中等难度)
给定一个不含重复数字的数组 `nums`,返回其所有可能的全排列。你可以按任意顺序返回答案。
解答代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() > 1)
result.push_back(path);
// uset的作用是对本层去重
unordered_set<int> uset;
for (int i = startIndex; i < nums.size(); i++) {
// ?
if ((!path.empty() && nums[i] < path.back()) ||
uset.find(nums[i]) != uset.end()) {
continue;
}
// 数层去重,每次递归都会定义新的uset,故不需要回溯
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
代码逻辑:
1. `permute` 函数:
- 用来启动回溯过程并返回最终的所有排列结果。初始化 `result` 和 `path` 并调用 `backtracking`。
- 创建一个 `used` 数组,表示每个元素是否已被使用。
2. `backtracking` 函数:
- 递归地构建排列。当 `path` 中的元素个数等于 `nums` 的大小时,表示一个完整的排列已经生成,加入 `result`。
- 遍历数组中的每个元素:
- 如果元素已经被使用,则跳过。
- 否则,加入当前元素到 `path`,标记为已使用,并继续递归调用 `backtracking`。
- 回溯时,移除 `path` 中的最后一个元素,并恢复其在 `used` 数组中的状态。
3. 最终返回的 `result` 包含了所有可能的排列。
代码难点:
1. 回溯的状态管理:通过 `path` 记录当前排列,通过 `used` 数组来标记元素是否被选择,确保每个元素只会在排列中出现一次。
2. 递归与回溯:递归逐层构建排列,每次回溯时通过 `pop_back()` 从 `path` 中移除元素,并恢复 `used` 数组中的状态,确保所有可能的排列都会被生成。