目录
491.递增子序列
-
文章讲解:programmercarl.com
回溯法
-
解题思路
-
在90.子集II (opens new window)中我们是通过排序,再加一个标记数组来达到去重的目的。
-
而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!
-
-
解题步骤
-
递归函数参数:本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
-
终止条件:本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题! (opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。但本题收集结果有所不同,题目要求递增子序列大小至少为2。
-
单层搜索逻辑:同一父节点下的同层上使用过的元素就不能再使用了。递归函数上面的
uset.insert(nums[i]);
,unordered_set<int> uset;
是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层,不需要回溯。
-
-
代码注意
-
path.back()
最后一个元素 -
nums[i]) != uset.end()
当前元素nums[i]
是否已经存在于unordered_set
(无序集合)uset
中
-
-
代码一:回溯法
// 时间复杂度: O(n * 2^n)
// 空间复杂度: O(n)
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() > 1) {
result.push_back(path);
// 注意这里不要加return,要取树上的节点
}
unordered_set<int> uset; // 使用set对本层元素进行去重
for (int i = startIndex; i < nums.size(); i++) {
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) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
46.全排列
-
题目链接:leetcode.cn
-
文章讲解:代码随想录
回溯法
-
解题思路
-
排列具有顺序,不可以直接把取过的前面全部去除,只需要记住当前使用的元素,前面的仍然可以用。
-
-
解题步骤
-
递归函数参数:元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。排列问题需要一个used数组,标记已经选择的元素。
-
递归终止条件:叶子节点,就是收割结果的地方。当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
-
单层搜索的逻辑:这里和77.组合问题 、131.切割问题和78.子集问题最大的不同就是for循环里不用startIndex了。因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
-
-
代码一:回溯法
// 时间复杂度: O(n!)
// 空间复杂度: O(n)
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking (vector<int>& nums, vector<bool>& used) {
// 此时说明找到了一组
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (used[i] == true) continue; // path里已经收录的元素,直接跳过
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
47.全排列 II (输入有重复)
-
题目链接:leetcode.cn
-
文章讲解:programmercarl.com
回溯法
-
解题思路
-
树层去重和树枝去重,去重一定要对元素进行排序。
-
组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
-
-
代码一:回溯法
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking (vector<int>& nums, vector<bool>& used) {
// 此时说明找到了一组
if (path.size() == nums.size()) {
result.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) {
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 排序
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
// 时间复杂度: 最差情况所有元素都是唯一的。复杂度和全排列1都是 O(n! * n) 对于 n 个元素一共有 n! 中排列方案。而对于每一个答案,我们需要 O(n) 去复制最终放到 result 数组
// 空间复杂度: O(n) 回溯树的深度取决于我们有多少个元素