• 代码随想录算法训练营DAY29(记录)|C++回溯算法Part.5|491.递增子序列、46.全排列、47.全排列II


    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)
      
      • 1
      • 2
      • 3
    • 终止条件:本题类似求子集问题,也要遍历树形结构找每一个结点,所以和78.子集类似,要遍历树形结构找每一个结点。其实可以不加终止条件,startIndex每次都会加1,并不会无限递归。本题收集结果有所不同,题目要求递增子序列大小至少为2,我们不要加return这样就可以取数上的所有结点啦:

    if (path.size() > 1) {
        result.push_back(path);
        // 注意这里不要加return,因为要取树上的所有节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 单层搜索逻辑

    同一父结点下的同层上使用过的元素就不能再使用了!这里我们使用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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    优化代码

    这里数值范围较小,我们使用数组来做哈希可以对代码进行优化:

    程序运行的时候对unordered_set 频繁的insertunordered_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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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)
    
    • 1
    • 2
    • 3
    • 递归终止条件:

      • 上图中可以看出,叶子结点收获结果,那么如何判断到了叶子结点呢,本题的判断很简单,path数组的大小达到和nums数组一样大的时候,就说明找到了全排列
      // 此时说明找到了一组
      if (path.size() == nums.size()) {
          result.push_back(path);
          return;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 单层搜索的逻辑:

      • 排列问题和之前写的组合、切割、子集问题最大的不同就是不用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;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    47.全排列II

    力扣题目链接

    文章链接:47.全排列II

    视频连接:回溯算法求解全排列,如何去重?| LeetCode:47.全排列 II

    状态:今天没时间了,随便记录一下!

    这题很有意思,这里给定的是一个包含了重复数字的序列,要返回所有不重复的全排列。很明显,需要去重

    那么我们这里要判断了,是树层去重呢,还是树枝去重?

    ![在这里插入图片描述]()
    if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
        continue;
    }
    
    • 1
    • 2
    • 3

    我们在40.组合总和II90.子集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) 回溯树的深度取决于我们有多少个元素
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
  • 相关阅读:
    Java Math.abs()如何获取绝对值呢?
    Redis简介
    华清 Qt day4 9月20
    z—libirary最新地址获取,zlibirary地址获取方式,zliabary最新地址,zliabary官网登录方式,zliabary最新登陆
    瑞吉外卖项目Day07-缓存优化
    怎么实现在微信公众号点外卖的功能
    FITC荧光素标记角叉菜胶;Carrageenan-FITC ;FITC-Carrageenan
    低成本实现webhook接收端[python]
    rust从0开始写项目-读取配置文件
    软件系统测试和验收测试有什么联系与区别?专业软件测试方案推荐
  • 原文地址:https://blog.csdn.net/caiziming_001/article/details/137891557