491.递增子序列
文档讲解:代码随想录 (programmercarl.com)
视频讲解:回溯算法精讲,树层去重与树枝去重 | LeetCode:491.递增子序列_哔哩哔哩_bilibili
状态:能直接写出来。不过还是要再看一遍,因为是新的去重类型以及递增类型。
思路
画了示例1的树形图,可以看出,用的是子集的方法:每个节点都要存入结果集。另外,还要进行树层去重,如图中绿色圈。由于该题不能对原数组排序,故只能用set。
画了示例2的树形图,可以看出,当前层选取的结点值必须小于上一层的节点值,才能出现递增,如下图红色圈。而上一层的节点值就是当前path数组的最后一个元素。
整体代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& nums, int startIndex){
if(path.size() >= 2){ //递增子序列中 至少有两个元素
result.push_back(path);
}
unordered_set<int> uset;//使用set来对本层元素进行去重,新的一层uset都会重新定义(清空),所以要知道uset只负责本层
for(int i = startIndex; i < nums.size(); ++i){
if(uset.find(nums[i]) != uset.end()) continue; //!树层去重
if(!path.empty() && nums[i] < path.back()) continue; //!当前元素小于上一层元素,则不递增,跳过
uset.insert(nums[i]);// 记录这个元素在本层用过了,本层后面不能再用了
path.push_back(nums[i]);
++i;
backtracking(nums, i);
--i;
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
优化
用数组来实现哈希。题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& nums, int startIndex){
if(path.size() >= 2){
result.push_back(path);
}
int used[201] = {0}; // 使用数组来对本层元素进行去重,新的一层数组都会重新定义(清空),所以要知道数组只负责本层!
for(int i = startIndex; i < nums.size(); ++i){
if(used[nums[i] + 100] == 1) continue; //!树层去重
if(!path.empty() && nums[i] < path.back()) continue; //!当前元素小于上一层元素,则不递增,跳过
used[nums[i] + 100] = 1;// 记录这个元素在本层用过了,本层后面不能再用了
path.push_back(nums[i]);
++i;
backtracking(nums, i);
--i;
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
46.全排列
文档讲解:代码随想录 (programmercarl.com)
视频讲解:组合与排列的区别,回溯算法求解的时候,有何不同?| LeetCode:46.全排列_哔哩哔哩_bilibili
状态:能直接做出来。
思路
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
- 由于排列是有序的,所以每次for循环都要从头开始,因此不用startIndex。
- 排列中每个元素只能被用一次,故用used数组表示某个元素是否被用过了,即树枝去重。而组合问题有startIndex,已达到树枝去重的效果。
- 当收集元素的数组path的大小和nums数组一样大时,说明找到一个全排列,即表示到达了叶子节点。
我写的代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
int used[21] = {0};
void backtracking(vector<int>& nums){
if(path.size() == nums.size()){ // 此时说明找到了一组
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); ++i){
if(used[nums[i] + 10] == 1) continue; // path里已经收录的元素,直接跳过
used[nums[i] + 10] = 1;
path.push_back(nums[i]);
backtracking(nums);
path.pop_back();
used[nums[i] + 10] = 0;
}
}
vector<vector<int>> permute(vector<int>& nums) {
backtracking(nums);
return result;
}
};
文档的代码:只是把全局used变成函数的一个形参而已。
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;
}
};
总结
排列问题的不同:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
47.全排列 II
文档讲解:代码随想录 (programmercarl.com)
视频讲解:回溯算法求解全排列,如何去重?| LeetCode:47.全排列 II_哔哩哔哩_bilibili
状态:能写出来。
思路
题目数组中有重复元素而要求结果中的各个集合不重复,所以用树层去重。由于可以重新排序,所以用数组进行树层去重。
由于**“数组进行树层去重”的数组used[i - 1] == false表示树层重复,used[i - 1] == true表示树枝重复**,所以不用再像上一题一样“开辟一个数组用于限制每个元素只能使用一次”,因为这是树枝去重,可以利用used来判断树枝重复,从而避免多开辟一个数组造成冗余。
我写的代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
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(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;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
拓展
去重最为关键的代码为:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
如果改成 used[i - 1] == true
, 也是正确的!,去重代码如下:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) continue;
这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用used[i - 1] == false
,如果要对树枝前一位去重用used[i - 1] == true
。
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
下面是[1, 1, 1]举例:
树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
可以很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。