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

本文主要是介绍代码随想录算法训练营DAY28(记录)|C++回溯算法Part.5|491.递增子序列、46.全排列、47.全排列II,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 491.递增子序列
    • 思路
    • 伪代码
    • CPP代码
    • 优化代码
  • 46.全排列
    • 思路
    • 伪代码
    • CPP代码
  • 47.全排列II
    • CPP代码

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

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

同一父结点下的同层上使用过的元素就不能再使用了!这里我们使用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();
}

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;}
};

优化代码

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

程序运行的时候对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;}
};

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

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

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

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;}
};

47.全排列II

力扣题目链接

文章链接:47.全排列II

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

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

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

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

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

我们在40.组合总和II、90.子集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) 回溯树的深度取决于我们有多少个元素

这篇关于代码随想录算法训练营DAY28(记录)|C++回溯算法Part.5|491.递增子序列、46.全排列、47.全排列II的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/913479

相关文章

C++中全局变量和局部变量的区别

《C++中全局变量和局部变量的区别》本文主要介绍了C++中全局变量和局部变量的区别,全局变量和局部变量在作用域和生命周期上有显著的区别,下面就来介绍一下,感兴趣的可以了解一下... 目录一、全局变量定义生命周期存储位置代码示例输出二、局部变量定义生命周期存储位置代码示例输出三、全局变量和局部变量的区别作用域

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

C++中NULL与nullptr的区别小结

《C++中NULL与nullptr的区别小结》本文介绍了C++编程中NULL与nullptr的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编... 目录C++98空值——NULLC++11空值——nullptr区别对比示例 C++98空值——NUL

C++ Log4cpp跨平台日志库的使用小结

《C++Log4cpp跨平台日志库的使用小结》Log4cpp是c++类库,本文详细介绍了C++日志库log4cpp的使用方法,及设置日志输出格式和优先级,具有一定的参考价值,感兴趣的可以了解一下... 目录一、介绍1. log4cpp的日志方式2.设置日志输出的格式3. 设置日志的输出优先级二、Window

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Java中调用数据库存储过程的示例代码

《Java中调用数据库存储过程的示例代码》本文介绍Java通过JDBC调用数据库存储过程的方法,涵盖参数类型、执行步骤及数据库差异,需注意异常处理与资源管理,以优化性能并实现复杂业务逻辑,感兴趣的朋友... 目录一、存储过程概述二、Java调用存储过程的基本javascript步骤三、Java调用存储过程示

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性: