1组合总和 II
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
**注意:**解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
思路:
-
递归函数的返回值以及参数:
- 返回值:递归函数
void backtracking(...)
没有返回值,通过引用参数vector<vector<int>>& result
来存储最终的结果。 - 参数:递归函数的参数包括候选数组
vector<int>& candidates
、目标值int target
、当前路径和int sum
、起始索引int startindex
、以及标记数组vector<bool>& used
。其中sum
表示当前路径的和,startindex
表示当前选择的起始位置,used
用于标记每个元素是否被使用过。
- 返回值:递归函数
-
回溯函数终止条件:
- 当
sum
等于target
时,表示当前路径的和等于目标值,将当前路径加入结果集中,并返回。
- 当
-
单层搜索的过程:
- 在单层搜索的过程中,通过遍历候选数组
candidates
,从startindex
开始,选取一个元素加入当前路径,更新sum
。 - 为了避免重复解,在选择下一个元素时,需要判断当前元素是否与上一个相同,并且上一个元素未被使用,若满足条件则跳过当前循环。
- 将选取的元素加入当前路径,并标记为已使用,然后递归调用
backtracking
函数,继续搜索下一个元素。 - 递归返回后,撤销当前元素的选择,即将当前元素标记为未使用,恢复
sum
,并从当前路径中移除当前元素,继续下一轮循环。
- 在单层搜索的过程中,通过遍历候选数组
重点过程:
去重的关键在于确保同一树层上相同的元素只被使用一次。在回溯过程中,如果发现当前元素与前一个元素相同,并且前一个元素未被使用过,那么就需要跳过当前循环,避免重复选择相同元素。
具体实现逻辑如下:
-
排序: 首先对候选数组进行排序,这样相同的元素就会相邻排列。
-
判断重复: 在遍历候选数组时,对于当前元素
candidates[i]
,如果它与前一个元素candidates[i - 1]
相同,那么需要判断前一个元素是否已经被使用过,即used[i - 1]
是否为 true。 -
跳过重复: 如果
candidates[i] == candidates[i - 1]
并且used[i - 1]
为 false,说明前一个树枝上已经使用过相同的元素candidates[i - 1]
,因此当前树枝上就不应该再使用candidates[i]
,直接跳过当前循环。
代码:
cpp
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;
}
// 从startindex开始遍历候选数组
for (int i = startindex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// 避免重复解,当当前元素等于前一个元素并且前一个元素未被使用时,跳过当前循环
if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
continue;
}
sum += candidates[i]; // 将当前元素加入路径和中
path.push_back(candidates[i]); // 将当前元素加入路径中
used[i] = true; // 标记当前元素已被使用
backtracking(candidates, target, sum, i + 1, used); // 递归搜索下一个元素
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); // 记录每个元素是否被使用过
sort(candidates.begin(), candidates.end()); // 排序输入数组,方便后续判断重复解
backtracking(candidates, target, 0, 0, used); // 调用回溯函数开始搜索
return result; // 返回结果集
}
};
2分割回文串
给你一个字符串 s
,请你将s
分割成一些子串,使每个子串都是
回文串
。返回 s
所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
提示:
1 <= s.length <= 16
s
仅由小写英文字母组成
思路:
-
递归函数的返回值以及参数: 在本题中,递归函数
backtracking
的返回值是void
,因为它主要用于收集满足条件的结果而不是返回单个结果。它的参数包括startindex
(当前索引)、s
(待分割的字符串)、path
(当前路径,存储当前分割的回文串)、result
(存储所有满足条件的分割方案)。 -
回溯函数终止条件: 切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。回溯函数的终止条件是当
startindex
大于等于字符串s
的长度时,说明已经完成了一次分割,需要将当前路径path
加入结果集result
中,并返回上一层继续搜索其他分割方案。 -
单层搜索的过程解题思路: 单层搜索过程是指在每一层递归中,我们进行路径选择和路径限制。路径选择是从当前索引
startindex
开始遍历字符串s
,选择以当前索引开始的所有可能子串作为候选解,并将其添加到当前路径path
中;路径限制是通过判断选取的子串是否为回文串来进行筛选。如果是回文串,则继续向下遍历;如果不是,则跳过当前选择,进行回溯。
重点过程:
在处理组合问题时,切割线通常表示了当前轮递归遍历的起始位置。在回溯过程中,我们需要确定每一次递归搜索的起点,这个起点就是切割线,决定了我们在当前轮次搜索的范围。
具体来说,在回溯函数中,切割线(也就是 `startIndex`)是一个很重要的参数,因为它告诉我们从哪里开始搜索下一个可能的解。每次递归调用时,我们都会更新切割线,使得下一次搜索的范围不会重复之前已经搜索过的部分。
对于回文串分割问题,切割线 `startIndex` 就是确定了下一次递归搜索时的起始位置。如果当前已经搜索到索引 `i` 的位置,下一次搜索就从 `i+1` 的位置开始,这样确保了不会重复搜索已经处理过的部分,也符合回溯算法的思路。
所以,在处理组合问题时,递归参数中的 `startIndex` 可以被理解为切割线,决定了每一轮递归搜索的起点,帮助我们避免重复搜索和有效地控制搜索范围。
代码:
cpp
class Solution {
private:
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++) {
// 如果从当前索引到 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) {
// 使用双指针判断是否为回文串
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
public:
// 分割回文串
vector<vector<string>> partition(string s) {
result.clear(); // 清空结果集
path.clear(); // 清空当前路径
backtracking(s, 0); // 开始回溯
return result; // 返回结果集
}
};
3子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
思路:
-
递归函数的返回值以及参数: 在这个例子中,递归函数
backtracking
的返回值是void
,因为它主要用于收集满足条件的结果而不是返回单个结果。它的参数包括nums
(原始数组)、startindex
(当前遍历的起始位置)。 -
回溯函数终止条件: 回溯函数的终止条件是当
startindex
大于等于nums
数组的长度时,说明已经完成了一轮搜索,需要返回上一层。剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了。 -
单层搜索的过程解题思路: 单层搜索过程是指在每一层递归中,我们进行路径选择和路径限制。路径选择是将当前元素加入到路径
path
中,然后递归搜索下一层;路径限制是确保在当前层递归中,不会重复选择已经选择过的元素。具体来说,我们从startindex
开始遍历原始数组nums
,将当前元素加入到路径path
中,然后递归调用下一层,并在递归完成后进行回溯,将当前元素从路径path
中移除,以便搜索其他分支。
代码:
cpp
class Solution {
public:
vector<int> path; // 存储当前路径的数组
vector<vector<int>> result; // 存储所有子集的数组
// 回溯函数,startindex表示当前遍历的起始位置
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) {
backtracking(nums, 0); // 调用回溯函数
return result; // 返回结果集
}
};