491.递增子序列
思路:本质是多个循环嵌套,一个一个选数,判断是否是递增的,但由于存在重复元素,需要使用used 来辅助判断是否选择过,这个重复元素。
在解决问题时将 unordered_set<int> used;
定义在 backtracking
函数内部而不是全局变量,主要是因为需要保证每个递归调用层级的状态是独立的,尤其是在处理需要在每个递归层级避免重复元素时。
-
递归层级的独立性:
- 局部变量 :当
used
定义在backtracking
函数内部时,每次进入函数时都会创建一个新的used
集合。这意味着每一层的递归调用都有自己的used
集合,它只用来记录并防止在当前递归层级中重复处理相同的元素。 - 全局变量 :如果
used
是全局变量,它会在所有递归层级之间共享,这会导致不能区分不同层级的元素使用情况。全局used
会阻止在不同层级中使用相同的元素,即使在不同的上下文中这样做是合法的。
- 局部变量 :当
-
回溯的正确性:
- 在递归问题中使用局部
used
变量,可以确保当递归函数执行回溯时,退出当前层级后,对应的used
集合也随之销毁,不会影响其他层级的状态。这是处理递归中状态回溯的常用和有效方法。 - 全局
used
变量需要手动管理添加和删除元素的操作,这增加了错误发生的可能性,尤其是在复杂的递归逻辑中。
- 在递归问题中使用局部
cpp
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int start) {
unordered_set<int> used; // 避免在同一层使用重复元素
for (int i = start; i < nums.size(); i++) {
// 跳过同一层已经使用过的元素
if (used.find(nums[i]) != used.end()) continue;
// 保证递增顺序
if (!path.empty() && nums[i] < path.back()) continue;
// 加入当前元素
used.insert(nums[i]);
path.push_back(nums[i]);
// 仅当路径长度大于1时,添加到结果中
if (path.size() > 1) {
result.push_back(path);
}
// 递归调用
backtracking(nums, i + 1);
// 回溯
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
46.全排列
cpp
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
vector<int> used[20];
void backtracking(vector<int>& nums, vector<int>& used){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++){
if(used[nums[i] + 10] == 0){
used[nums[i] + 10] = 1;
path.push_back(nums[i]);
backtracking(nums, used);
used[nums[i] + 10] = 0;
path.pop_back();
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> used(21);
backtracking(nums, used);
return result;
}
};
排列问题的不同:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
47.全排列 II
这题由于包含重复元素,所以代码比全排列多了个判断分支,用于去重。
used[i-1] == false 的含义:只有当递归回溯回进到 for 循环的下一个 i 时,才可能出现 i-1 和 i 的值相等,但 used[i-1] 的标记时 false 的情况,即当前处于同一递归层级中。
与组合总和Ⅱ的那个去重的逻辑相似,都是树层去重。
这题当然也可以使用 used[i-1] == true 这个判断进行树枝去重,但效率很低。详细可以看代码随想录这题的详细解释,有两张图辅助理解。
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(i > 0 && nums[i-1] == nums[i] && used[i-1] == false){
continue;
}
if(used[i]) continue;
used[i] = 1;
path.push_back(nums[i]);
backtracking(nums, used);
used[i] = 0;
path.pop_back();
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used;
used.assign(nums.size(), false);
sort(nums.begin(), nums.end());
backtracking(nums, used);
return result;
}
};
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。