1子集 II
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
回溯思路:
回溯三部曲:
- 回溯函数模板返回值以及参数
- 回溯函数终止条件
- 回溯搜索的遍历过程
回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
-
递归函数的返回值以及参数:
backtracking
函数的返回类型是void
,因为它的主要作用是通过修改全局变量result
来存储所有符合条件的子集。- 参数包括
nums
(原始数组)、startindex
(当前遍历的起始位置)、used
(记录元素是否被使用过的数组)。
-
回溯函数终止条件:这里没有明确的终止条件判断
startindex >= nums.size()
,因为要考虑处理重复元素的情况。实际上,整个数组都要遍历到,因此终止条件是在每次递归时自然完成的。 -
单层搜索的过程解题思路:
- 在每一层递归中,首先将当前的
path
路径加入到result
结果集中,即使它可能是重复的子集。 - 接下来遍历从
startindex
开始的nums
数组元素,确保在同一层级不重复选择相同的元素。这里通过排序数组和判断相邻元素是否相同来避免重复选择问题。 - 如果发现当前元素和上一个相同,并且上一个元素未被使用过(即
!used[i-1]
),则跳过当前元素,以确保不重复选择同一层级的相同元素。 - 否则,将当前元素加入
path
中,标记为已使用,然后递归进入下一层级。 - 在递归返回后,撤销选择,即将当前元素的标记置为未使用,同时从
path
中移除当前元素,以进行回溯到上一层决策树。
- 在每一层递归中,首先将当前的
-
去重部分的实现:
- 在
backtracking
函数中,通过排序原始数组nums
来确保重复元素相邻。 - 在每次递归选择元素时,判断当前元素是否与上一个相同且上一个元素未被使用过,若是,则跳过当前元素的选择,以避免重复生成相同的子集。
- 在
代码:
class Solution {
private:
vector<vector<int>> result; // 最终结果存储的容器
vector<int> path; // 当前路径的容器
void backtracking(vector<int>& nums, int startindex, vector<bool>& used) {
result.push_back(path); // 将当前路径加入最终结果
// 遍历选择列表
for (int i = startindex; i < nums.size(); ++i) {
// 剪枝条件:确保不重复选择同一层级的相同元素
// 如果同一树层的相邻元素相同且前一个元素未被使用过,则跳过
if (i > startindex && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
path.push_back(nums[i]); // 做出选择
used[i] = true; // 记录选择
backtracking(nums, i + 1, used); // 进入下一层决策树
used[i] = false; // 撤销选择
path.pop_back(); // 回溯,撤销选择
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(), false); // 记录元素是否被使用过
sort(nums.begin(), nums.end()); // 排序,为了去重
backtracking(nums, 0, used); // 调用回溯算法
return result; // 返回最终结果
}
};
2非递减子序列
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1] 输出:[[4,4]]
提示:
1 <= nums.length <= 15
-100 <= nums[i] <= 100
思路:
-
递归函数的返回值以及参数:
backtracking
函数用于在nums
数组中查找所有递增子序列,并将结果存储在result
中。- 参数包括
nums
数组和startindex
,表示当前递归的起始位置。
-
回溯函数终止条件:
- 当
path
中的元素个数大于1时,将path
加入到result
中,因为题目要求子序列长度至少为2。
- 当
-
单层搜索的过程:
- 使用
uset
(unordered_set<int>
)来进行元素的去重操作,确保同一层级(即同一递归深度)中不重复选择相同的元素。 - 在
for
循环中,从startindex
开始遍历nums
数组。 - 对于每一个元素
nums[i]
,检查是否满足以下两个条件之一:- 如果
path
不为空且nums[i] < path.back()
,则说明当前元素不符合递增要求,跳过。 - 如果
nums[i]
已经在uset
中存在,也跳过,避免重复选择。
- 如果
- 若满足条件,则将
nums[i]
加入到path
和uset
中,并递归调用backtracking(nums, i + 1)
继续寻找下一个元素。 - 递归完成后,需要进行回溯操作,即从
path
中移除当前添加的元素,以便尝试其他可能的组合。
- 使用
-
去重部分的实现:
- 使用
uset
来存储已经选择过的元素,保证在同一层级中不会重复选择相同的元素,从而避免生成重复的子序列。
- 使用
代码:
class Solution {
private:
vector<vector<int>> result; // 存储最终结果的二维数组
vector<int> path; // 回溯过程中当前的子序列
void backtracking(vector<int>& nums, int startindex) {
// 当前子序列长度大于1时,将其加入结果集
if (path.size() > 1) {
result.push_back(path);
}
unordered_set<int> uset; // 用于记录当前已经选择过的元素
// 从startindex开始遍历nums数组
for (int i = startindex; i < nums.size(); i++) {
// 如果当前path不为空且当前元素小于path中最后一个元素,跳过该元素
// 或者当前元素已经在uset中出现过,也跳过该元素
if (!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end()) {
continue;
}
// 将当前元素加入path和uset
uset.insert(nums[i]);
path.push_back(nums[i]);
// 递归调用backtracking,从i+1位置开始继续寻找
backtracking(nums, i + 1);
// 回溯,将当前元素从path中移除
path.pop_back();
}
}
public:
// 主函数,寻找nums数组中的所有递增子序列
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0); // 调用回溯函数
return result; // 返回最终结果
}
};
3全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1] 输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1] 输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
思路:
1. 递归函数的返回值以及参数
返回值和参数:
void backtracking(vector<int>& nums, vector<bool>& used)
- 返回值为
void
,因为我们不需要从每次调用中返回特定的值,而是通过引用参数来改变全局的result
和path
。 - 参数
nums
是输入的原始数组,used
是一个标记数组,用于记录每个位置的元素是否已经被使用过。
- 返回值为
2. 回溯函数的终止条件
终止条件:
if (path.size() == nums.size())
- 当
path
的长度等于nums
的长度时,表示已经形成了一个完整的排列,此时将path
加入到result
中,然后返回。
- 当
3. 单层搜索的过程
解题思路:
- 选择路径: 在每一层的递归中,遍历
nums
数组的所有元素。 - 判断条件: 使用
used
数组来判断当前元素是否已经被使用过,如果已经被使用过,则跳过。 - 标记和递归:
- 将当前未使用的元素加入
path
中,并标记为已使用 (used[i] = true
)。 - 递归调用
backtracking
,继续向下一层搜索。 - 在递归返回后,执行回溯操作:将
path
的最后一个元素移除 (path.pop_back()
),并将当前位置的标记恢复为未使用 (used[i] = false
),以便进行下一次选择。
- 将当前未使用的元素加入
去重部分:
- 在排列问题中,为了避免重复,需要确保每次选择的元素都是从未被使用过的元素中选择。
- 去重部分: 在本题中,通过
used
数组来标记每个元素是否已经使用过,从而避免重复选择相同的元素。
for
循环遍历时i=0
与之前的i=startindex
的区别for
循环遍历: 在回溯的过程中,每次递归调用时,通过for
循环遍历整个nums
数组。与上面的回溯算法相比,我们并没有显式地使用startindex
,而是每次从i=0
开始遍历整个数组。这是因为在每一层递归中,我们需要考虑所有未被使用过的元素,而不是仅从某个特定位置开始。
代码:
class Solution {
private:
vector<vector<int>> result; // 存储最终结果的二维向量
vector<int> path; // 存储当前路径的一维向量
// 回溯函数,参数为原始数组nums和标记数组used
void backtracking(vector<int>& nums, vector<bool>& used) {
// 当路径长度等于数组长度时,将当前路径加入结果集合
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
// 遍历数组nums
for (int i = 0; i < nums.size(); i++) {
// 如果元素已经被使用过,则跳过
if (used[i]) continue;
// 标记当前元素为已使用
used[i] = true;
// 将当前元素加入路径中
path.push_back(nums[i]);
// 递归进入下一层决策树
backtracking(nums, used);
// 回溯操作,撤销选择
path.pop_back();
// 恢复当前元素为未使用
used[i] = false;
}
}
public:
// 主函数,生成所有排列的入口函数
vector<vector<int>> permute(vector<int>& nums) {
// 标记数组,记录每个位置的元素是否被使用过
vector<bool> used(nums.size(), false);
// 调用回溯函数,从第一个位置开始生成排列
backtracking(nums, used);
// 返回最终的结果集合
return result;
}
};