文章目录
- 491.递增子序列
- 思路
- 伪代码
- CPP代码
- 优化代码
- 46.全排列
- 思路
- 伪代码
- CPP代码
- 47.全排列II
- CPP代码
491.递增子序列
力扣题目链接
文章链接:491.递增子序列
视频连接:回溯算法精讲,树层去重与树枝去重 | LeetCode:491.递增子序列
状态:今天没时间了,随便记录一下!
思路
本题与90.子集II非常类似。
也就是本题的递增子序列有要求子集,然后还要去重,但是:本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!
这里用[4, 7, 6, 7]进行对比。
伪代码
-
递归函数参数:求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& nums, int startIndex)
-
终止条件:本题类似求子集问题,也要遍历树形结构找每一个结点,所以和78.子集类似,要遍历树形结构找每一个结点。其实可以不加终止条件,startIndex每次都会加1,并不会无限递归。本题收集结果有所不同,题目要求递增子序列大小至少为2,我们不要加
return
这样就可以取数上的所有结点啦:
if (path.size() > 1) {
result.push_back(path);
// 注意这里不要加return,因为要取树上的所有节点
}
- 单层搜索逻辑
同一父结点下的同层上使用过的元素就不能再使用了!这里我们使用set
来进行去重。
使用set只记录本层元素是否重复使用,新的一层uset都会被重新定义(清空)。
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();
}
CPP代码
// 版本一
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;
}
};
优化代码
这里数值范围较小,我们使用数组来做哈希可以对代码进行优化:
程序运行的时候对unordered_set
频繁的insert
,unordered_set
需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。
// 版本二
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);
}
int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())
|| used[nums[i] + 100] == 1) {
continue;
}
used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了
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.全排列
46.全排列
文章链接:46.全排列视频连接:组合与排列的区别,回溯算法求解的时候,有何不同?| LeetCode:46.全排列
状态:今天没时间了,随便记录一下!
思路
排列与组合(包括分割和子集都近似于组合问题,所以我们总是处理前对其进行排序)相比,排列是有顺序的。总而言之排列是强调元素顺序的。
全排列问题的树形结构如图:
从树形结构可以看出,我们每次都是从头开始,即使元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。
这里我们会使用used数组,记录此时path里都有哪些元素使用啦,一个排列里一个元素只能使用一次。
伪代码
- 递归函数参数:正如上文中所讲的,我们需要一个used数组
vector<vector<int>> result;
vector<int> path;
void backtracking (vector<int>& nums, vector<bool>& used)
-
递归终止条件:
- 上图中可以看出,叶子结点收获结果,那么如何判断到了叶子结点呢,本题的判断很简单,path数组的大小达到和nums数组一样大的时候,就说明找到了全排列。
// 此时说明找到了一组 if (path.size() == nums.size()) { result.push_back(path); return; }
-
单层搜索的逻辑:
- 排列问题和之前写的组合、切割、子集问题最大的不同就是不用startindex来指明遍历的位置了,因为我们每次都要从头开始搜索,正如上文描述的那样。这里我们使用used。
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; }
CPP代码
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
力扣题目链接
文章链接:47.全排列II
视频连接:回溯算法求解全排列,如何去重?| LeetCode:47.全排列 II
状态:今天没时间了,随便记录一下!
这题很有意思,这里给定的是一个包含了重复数字的序列,要返回所有不重复的全排列。很明显,需要去重!
那么我们这里要判断了,是树层去重呢,还是树枝去重?
![在这里插入图片描述]()if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
我们在40.组合总和II、90.子集II中已经详细论述过去重逻辑的写法,这里直接给出代码
CPP代码
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) 回溯树的深度取决于我们有多少个元素