491.递增子序列
先绘制树形结构
本题看图,发现好像也是需要去重,去的也是站在某个节点的时候,可选择的重复路径
此外我们补充一点,为什么一定需要去掉站在某个节点时可选择的重复路径?看图:在剩余集合为{4,7,6,7}的节点时我们取序列中从左到右的第一个元素7,走这条路径会到一个剩余集合为第一个7之后的节点; 而如果我们取从左到右的第二个元素7,会走到一个剩余集合为第二个7之后的节点。然而,取第一个7之后剩余能取的集合包含了取第二个7之后剩余能取的集合,拟人化的理解是:走这条路能够到达的地方已经完全包含了走那条路能够到达的地方,因此我们就走这条路就行了,别走那条路了。这就是我们需要去掉单层中重复路径的原因
怎么树层去重,之前都有讲过,基本上是通过排序+记录在该节点的已选择路径实现。问题是本题需要求子序列,而一个序列的子序列一定是按照原序列顺序的,也就是说我们不能对原序列进行排序了
让我们回到Day27,看看需要排序的原因
取1然后能取7,取7然后能取1,会出现相同的组合;如果从小到大排序后取1然后能取7,但取7后就不能取1了,就不会出现相同组合,因此我们需要排序。再强调一下我们取元素都是从左向右取的(即从索引由小到大取)
上面介绍的是在组合问题中需要排序的原因。然而,{1,7}和{7,1}是相同组合,可是他们并不是相同的子序列!那就简单了,这题我们根本不需要排序,因为组合问题中出现的重复状况这里不会出现,我们这里只需要单纯去掉站在某个节点时可选择的重复路径就行了
上代码
class Solution {
private:
vector<int> path; // 用于在回溯遍历节点的过程中记录子序列的元素
vector<vector<int> > result; // 用于存放子序列结果
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
void backtracking(vector<int> & nums, int startIndex) { // startIndex用于标识站在一个节点有哪些路径可选
if (path.size() >= 2)
result.push_back(path); // 我们到了一个节点,只要满足条件就需要收集结果,而不是等到叶子节点才收集结果
if (startIndex >= nums.size())
return; // 终止条件:startIndex大于数组最大索引值
unordered_set<int> used; // 记录已经走过的路径
for (int i = startIndex; i < nums.size(); ++i) { // 依据startIndex来选择路径
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]); // 做记录
backtracking(nums, i + 1); // 前往这样一个节点:站在该节点可选择数组从索引i+1到末尾的所有元素
path.pop_back(); // 回溯撤销记录
}
return;
}
};
详见代码注释
46.全排列
排列问题和组合问题不同,相同元素不同顺序是相同组合,但是不同排列
树形结构
本题和组合问题不一样的是,比如组合问题里面从剩余集合为{1,2,3}的集合里选择了2之后,到达的节点就不能再选择1了,否则会和先选择1再选择2重复。在组合问题中我们是通过startIndex控制在一个节点时的剩余可选元素的
而排列问题中,就算选择2之后,也能选择1,先选择2再选择1得到的{1,2}和先选择2再选择1得到的{2,1}不是一个排列。因此在一个新节点,怎么确定在该节点时能够选择的元素?(即如何确定在一个节点有哪些路可去)答:只要是还没被选择过的元素,都可以作为可选路径,我们通过一个used数组来标记已经选择的元素,从而控制在当前节点的可选择路径。这个used数组也能够标记节点
看代码
class Solution {
private:
vector<int> path;
vector<vector<int> > result;
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<int> used(nums.size(), 0); // used一开始为和nums大小相同的全0数组,表示“一个元素都还没选”
backtracking(nums, used);
return result;
}
void backtracking(vector<int> & nums, vector<int> & used) { // 需要传入used数组
if (path.size() == nums.size()) { // 终止条件 收集结果 返回
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) { // 遍历所有可选择路径(即遍历所有可选择元素)
if (used[i] == 1) continue; // 如果这个元素已经被选择了,则跨过这个元素
used[i] = 1;
path.push_back(nums[i]); // 添加记录
backtracking(nums, used); // 前往下一个节点,通过used标记该节点
used[i] = 0;
path.pop_back(); // 撤销记录,需同时维护path和used
}
return;
}
};
47.全排列II
本题又是缝合怪,排列加上去除在某节点时可去的重复路径
为什么要去重? 拟人化的理解是:走这条路能够到达的地方已经完全包含了走那条路能够到达的地方,因此我们就走这条路就行了,别走那条路了。这就是我们需要去掉单层中重复路径的原因
代码如下
class Solution {
private:
vector<int> path;
vector<vector<int> > result;
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<int> used(nums.size(), 0);
backtracking(nums, used);
return result;
}
void backtracking(vector<int> & nums, vector<int> & used) { // used用于标记节点,控制在该节点有哪些路径可去
if (path.size() == nums.size()) // 终止条件
{
result.push_back(path);
return;
}
unordered_set<int> uset; // 用于记录在当前节点已经走过的路径
for (int i = 0; i < nums.size(); ++i) {
if (uset.find(nums[i]) != uset.end()) continue; // 如果这条路径已经走了,就跨过这条路径去往下一个路径
if (used[i] == 1) continue; // 通过used控制可以选择的元素,used[i] = 1表明对应的nums[i]已经被选择过了,在该层节点没有选择nums[i]的这条路径
uset.insert(nums[i]); // 标记该路径为“已走过”
used[i] = 1;
path.push_back(nums[i]); // 添加记录同时维护path和used
backtracking(nums, used);
used[i] = 0;
path.pop_back(); // 撤销记录同时维护path和used
}
return;
}
};
几乎是相同的去重逻辑,用set记录当前节点已经走过的路径,从而进行去重
这里没有先在主函数对元素进行排序的原因:不同顺序的相同元素其实是不同排列,因此40.组合总和II中没有排序所产生的问题在这里都不是问题(当然这里先排序也可以)