Day29【回溯算法】491.递增子序列、46.全排列、47.全排列II

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中没有排序所产生的问题在这里都不是问题(当然这里先排序也可以)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林沐华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值