491.递增子序列
- 思路:
- 分析题目:
- 输入一个序列,输出至少有两个元素的递增子序列。所谓序列,就是按照次序排好的行列,因此本题不可以把输入数组重新排序,否则就会改变序列的顺序。因此,不能使用前面90.子集2中的去重逻辑。
- 本题要取的递增子序列,也就是取有序的子集,且不能有相同的递增子序列。
- 回溯三部曲:
- 函数参数:需要startIndex,调整下一层递归的起始位置,防止取到重复的元素。
- 终止条件:相比组合问题收集叶子节点,子集问题收集所有节点,本题更类似子集问题,只是对子集加了限定条件,也就是递增子序列大小至少为2,因此取的不是所有节点。
- 单层逻辑:
- 假设给定一个数组[1,3,3,4,2],横向遍历到第二个3时,以它开头的递增子序列会和前面遍历的3一样,因此要去重,使用一个set来存储遍历过的元素,如果当前遍历到的元素已经存在set中了,就continue遍历下一个元素。
- 当遍历到2时,如果此时path中末尾是元素3或4,那么加入2就无法构成递增子序列,这种情况也直接continue遍历下一个元素。
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n)
- 优化:用数组来做哈希
- 分析题目:
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums, int startIndex) {
//当path中至少有两个元素时才收集结果
if(path.size() > 1) res.push_back(path);
unordered_set<int> uset;//使用set在横向遍历时去重,由于每次递归都会重新定义一个新的set,因此不需要对set进行回溯
for(int i = startIndex; i < nums.size(); i++) {
//如果path中有元素,且当前遍历到的元素小于path中最后一个(最大)的元素
//或 当前遍历元素,已有相同的元素被用过了
//直接continue继续遍历下一个元素
if(!path.empty() && nums[i] < path.back()
|| uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);//记录这个元素在本层用过了,本层后面不能再用了
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums, 0);
return res;
}
};
46.全排列
- 思路:排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这是和之前分析的子集以及组合所不同的地方。
- 回溯三部曲:
- 函数参数:排列问题就不需要使用startIndex来去重了,但是需要标记已经使用过的元素,防止排列中出现重复的元素。使用used数组,标记已经选择的元素。
- 终止条件:叶子节点就是要收集的结果,当path的大小等于输入数组的大小时,就说明是叶子节点,收集结果。
- 单层逻辑:排列问题,每次都要从头开始搜索,但是一个排列里一个元素只能使用一次,所以要用used数组标记使用过的元素。
- 时间复杂度: O(n!)
- 空间复杂度: O(n)
- 回溯三部曲:
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums) {
if(path.size() == nums.size()) {
res.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++) {
//如果path中已经有当前元素,直接continue遍历下一元素,防止元素重复
if(count(path.begin(), path.end(), nums[i])) {
continue;
}
path.push_back(nums[i]);
backtracking(nums);
path.pop_back();
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums);
return res;
}
};
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums, vector<bool>& used) {
if(path.size() == nums.size()) {
res.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++) {
//如果path中已经有当前元素,直接continue遍历下一元素,防止元素重复
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) {
res.clear();
path.clear();
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return res;
}
};
47.全排列II
- 思路:
- 与上一题的区别在于,输入数组可包含重复数字,要返回所有不重复的全排列。因此需要进行去重。去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
- 横向遍历时(for循环),如果前一个元素的值与当前遍历元素的值相同,那么就进行去重。
- 使用used数组进行标记,以[1,1,2]为例:纵向遍历到第二个1时,used数组中第一位应该为1,因为纵向上元素可以重复;横向遍历到第二个1时,used数组中第一位应该为0,这是上一层回溯后的结果,说明横向遍历过程中已经使用过1了,直接跳过。
- 时间复杂度: 最差情况所有元素都是唯一的。复杂度和全排列1都是 O(n! * n) 对于 n 个元素一共有 n! 中排列方案。而对于每一个答案,我们需要 O(n) 去复制最终放到 result 数组。
- 空间复杂度: O(n) 回溯树的深度取决于我们有多少个元素
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums, vector<bool>& used) {
if(path.size() == nums.size()) {
res.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++) {
// used[i - 1] == true,说明同一树枝nums[i - 1]使用过
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
if(used[i] == false) {
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
res.clear();
path.clear();
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return res;
}
};
总结
前面跳过了used做法,现在还是遇到了,还要再多理解练习
参考链接
代码随想录:递增子序列 全排列 全排列II