1.回溯算法.题目

1.回溯算法.题目

  • 题目
    • 9.子集问题
    • 10.子集||
    • 11.递增子序列
    • 12.全排列
    • 13.全排列||
    • 14.回溯算法去重问题的另外一个写法
    • 15.重新安排行程
    • 16.N皇后
  • 总结
    • 去重方式的不同

题目

9.子集问题

(题目链接)
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集,包含空集和子集)。说明:解集不能包含重复的子集,即不能存在元素组成一样的集合。
在这里插入图片描述
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而==子集问题是找树的所有节点!==这与之前的组合问题不同,子集问题也是一种组合问题,它的集合时无序的(因此取过的元素不会重复取,在回溯算法时会用到startindex作为每层树的遍历起点)。

    std::vector<std::vector<int>> res;
    std::vector<int> path;
    void backtracking(std::vector<int>& nums, int startindex){
        //res.push_back(path); //加入这一行,则不用先加入空集,也能保证自身能在res中
        //其实不需要加终止条件,因为startindex>=nums.size(),本层循环就结束了
        if(startindex>=nums.size()) return; 
        for(int i=startindex; i<nums.size();i++){
            path.push_back(nums[i]);
            res.push_back(path);
            backtracking(nums, i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        res.clear();
        path.clear();
        res.push_back(path);
        backtracking(nums, 0);
        return res;
    }

时间复杂度: O(n * 2^n);空间复杂度: O(n)


10.子集||

(题目链接)
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。例如:输入: [1,2,2],输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]。
这题与9.题的区别是给定集合里存在重复元素,而且求取的子集要去重;去重与6.组合||里的去重是一直的,所以理解“树层去重”和“树枝去重”非常重要
在这里插入图片描述
从以上图解可以看出从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集

    std::vector<std::vector<int>> res;
    std::vector<int> path;
    void backtracking(std::vector<int>& nums, int startindex, std::vector<bool> used){
        res.push_back(path);
        for(int i=startindex; i<nums.size(); i++){
            if(i>0 && nums[i]==nums[i-1] && used[i-1]==false) continue; // 说明是同层重复
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums, i+1, used);
            used[i]=false;
            path.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        res.clear();
        path.clear();
        std::vector<bool> used(nums.size(), false);
        std::sort(nums.begin(), nums.end());
        backtracking(nums, 0, used);
        return res;
    }

时间复杂度: O(n * 2^n);空间复杂度: O(n)


11.递增子序列

(题目链接)
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。示例:输入: [4, 6, 7, 7];输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]。说明:给定数组的长度不会超过15,数组中的整数范围是 [-100,100],给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
本题与之前的的求子集问题不同,这题是求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能用之前的去重逻辑!
在这里插入图片描述

    std::vector<std::vector<int>> res;
    std::vector<int> path;
    void backtracking(std::vector<int>& nums, int startindex){
        if(path.size()>1) res.push_back(path);
        std::unordered_set<int> uset; //使用set对本层元素去重
        for(int i=startindex; i<nums.size(); i++){
            // 当1.不是递增 2.已经在set中存在 则使用continue跳过本层这一递归
            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();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        res.clear();
        path.clear();
        backtracking(nums, 0);
        return res;
    }

因为不能使用std::sort对给定数组进行nums预排列,所以不能使用std::vector<int> used布尔值数组的方式去重。


12.全排列

(题目链接)
给定一个 没有重复 数字的序列,返回其所有可能的全排列。不同于集合,排列时有序的,即 [1,2] 和 [2,1] 是两个集合。但排列问题需要一个used布尔值数组,标记已经选择的元素。
在这里插入图片描述
从上图可以看出,叶子节点就是得出结果的地方,即当path.size()==nums.size()时,长度一致时即得到了一种全排列的结果。

    std::vector<std::vector<int>> res;
    std::vector<int> path;
    void backtracking(std::vector<int>& nums, std::vector<bool> used){
        if(path.size()==nums.size()) {
            res.push_back(path);
        }
        for(int i=0; i<nums.size(); i++){
            if(used[i]==true) continue; //当历史层已经使用过该位置的元素时,则跳过本层中该次递归。
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        res.clear();
        path.clear();
        std::vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return res;
    }

排列问题的不同:每层都是从0开始搜索而不是startIndex,需要used数组记录path里都放了哪些元素了.


13.全排列||

(题目链接)
给定一个 可包含重复数字 的序列 nums ,按任意顺序 返回所有不重复的全排列。例如:输入:nums = [1,1,2];输出: [[1,1,2], [1,2,1], [2,1,1]]。因此本题又涉及去重,要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
在这里插入图片描述

    std::vector<std::vector<int>> res;
    std::vector<int> path;
    void backtracking(std::vector<int>& nums, std::vector<bool> used){
        if(path.size()==nums.size()){
            res.push_back(path);
            return;
        }
        for(int i=0; i<nums.size(); i++){
        	// used[i - 1] == false,说明同一树层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;
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        res.clear();
        path.clear();
        std::sort(nums.begin(), nums.end());
        std::vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return res;
    }

时间复杂度: O(n! * n);空间复杂度: O(n)


14.回溯算法去重问题的另外一个写法

子集||:在使用std::unordered_set实现去重,正确的做法是,在for循环前,创建一个set去存储在for遍历同一层节点时出现过的元素。这样就算进入下一层的递归,也能保证本层的used不受影响。
1.错误的做法一:将set定义在类成员位置(相当于全局变量),然后模拟回溯的样子 insert一次,erase一次,不是单纯地控制某一节点下的同一层了,这样就是控制整棵树,包括树枝
2.错误做法二:把 unordered_set uset; 放到类成员位置,然后每次进入单层的时候用uset.clear()。uset已经是全局变量,本层的uset记录了一个元素,然后进入下一层之后这个uset(和上一层是同一个uset)就被清空了(backtracking()),也就是说,层与层之间的uset是同一个,那么就会相互影响

组合总和||,全排列 II:在使用std::unordered_set实现去重,正确的做法是,在for循环前,创建一个set去存储在for遍历同一层节点时出现过的元素;或者使用used布尔值数组+排序的方式实现去重。使用set去重的版本相对于used数组的版本效率都要低很多。因为回溯的过程需要频繁对set insert,然后unordered_set需要做哈希映射,这相对耗时间,而使用used数组在时间复杂度上几乎没有额外负担


15.重新安排行程

(题目链接)
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。题目有点抽象,说明:
1.如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前
2.所有的机场都用三个大写字母表示(机场代码)
3.假定所有机票至少存在一种合理的行程。
4.所有的机票必须都用一次且只能用一次。
在这里插入图片描述
这题难度很大!有点像图论里深度优先搜索的味道,亦可用回溯的方法解决。题目的难点有:

  1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环?
    出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环。
  2. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢?
    == 一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场==,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。这样存放映射关系可以定义为 unordered_map<string, multiset<string>> targets 或者 unordered_map<string, map<string, int>> targets。在搜索过程要不断的删multiset里的元素,那么推荐使用unordered_map<string, map<string, int>> targets。在遍历unordered_map<出发机场, map<到达机场, 航班次数>> targets 的过程中,使用”航班次数“这个字段的数字做相应的增减,来标记到达机场是否使用过了。如果”航班次数“大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了。-就是不对该机场作操作而是标记一下。可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效
  3. 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
    递归函数参数和返回值:参数传入int ticketNum航班数量, std::vector<string>& result结果容器,递归函数返回bool值,因为这题相当于找到一条合适的通路通向叶子节点,使得机场能连贯起来。
    终止条件:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程
    单层递归的逻辑:
    std::unordered_map<std::string, std::map<std::string, int>> targets;
    bool backtracking(std::vector<std::string>& res, int ticknum){
        if(res.size()==ticknum+1) return true;
        for(std::pair<const std::string, int>& target : targets[res[res.size()-1]]){
            if(target.second>0){
                res.push_back(target.first);
                target.second--;
                if(backtracking(res, ticknum)) return true;
                target.second++;
                res.pop_back();
            }
        }
        return false;
    }
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        std::vector<std::string> res;
        for(const std::vector<std::string>& vec: tickets){
            targets[vec[0]][vec[1]]++;
        }
        res.push_back("JFK");
        backtracking(res, tickets.size());
        return res;
    }

16.N皇后

(题目链接)
与之前一维数组的子集,排列问题不同,这次处理的是二维棋盘问题。

    std::vector<std::vector<std::string>> res;
    void backtracking(int n, int row, std::vector<std::string>& chessboard){
        if(row==n){
            res.push_back(chessboard);
            return;
        }
        for(int i=0; i<n; i++){
            if(isVaild(n, row, i, chessboard)){
                chessboard[row][i]='Q';
                backtracking(n, row+1, chessboard);
                chessboard[row][i]='.';
            }
        }
    }
    bool isVaild(int n, int row, int col, std::vector<std::string>& chessboard){
        // 检查同一列
        for(int i=0; i<row; i++){
            if(chessboard[i][col]=='Q') return false;
        }
        // 检查45°角
        for(int i=row-1, j=col+1; i>=0 && j<n; i--, j++){
            if(chessboard[i][j]=='Q') return false;
        }
        // 检查135°角
        for(int i=row-1, j=col-1; i>=0 && j>=0; i--, j--){
            if(chessboard[i][j]=='Q') return false;
        }
        return true;
    }
    vector<vector<string>> solveNQueens(int n) {
        res.clear();
        std::vector<std::string> chessboard(n, std::string(n, '.'));
        backtracking(n, 0, chessboard);
        return res;
    }

17.解数独
(题目链接)
题目跟定一个有std::vector<std::vector>& board 三维带有1~9数字以及“."的9x9二维矩阵,我们需要就该矩阵完成数独的求解,并补充完成该矩阵。
递归函数从参数和返回值:找到根节点到叶子节点一条唯一的路径,需要bool返回值,输入参数则是二维矩阵
终止条件:不需要终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。但不会陷入死循环嘛?
单层递归逻辑:两层for循环分别遍历row,col,然后在使用一个for循环,填充"1"~"9"的字符,并在填入该字符前也是需要经过isVaild函数判断该位置是否有效,若有效则返回true,否则当填充字符的for循环结束后,还没找到合适的字符,则返回false
isVaild函数:判断所在row,col上,k字符是否是独立存在的。

    bool backtacking(std::vector<std::vector<char>>& board){
        for(int i=0; i<9; i++){
            for(int j=0; j<9; j++){
                if(board[i][j]!='.') continue;
                for(char k='1'; k<='9'; k++){
                    if(isVaild(i, j, k, board)){
                        board[i][j]=k;
                        if(backtacking(board)) return true;
                        board[i][j]='.';
                    }
                }
                return false;

            }
        }
        return true;
    }

    bool isVaild(int row, int col, char val, std::vector<std::vector<char>>& board){
        // 检查行
        for(int i=0; i<9; i++){
            if(board[row][i]==val) return false;
        }
        // 检查列
        for(int i=0; i<9; i++){
            if(board[i][col]==val) return false;
        }
        // 检查所在九宫格
        int startrow = (row/3)*3;
        int startcol = (col/3)*3;
        for(int i=startrow; i<startrow+3; i++){
            for(int j=startcol; j<startcol+3; j++){
                if(board[i][j]==val) return false;
            }
        }
        return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtacking(board);
    }

注意isvaild函数里关于9宫格里数字重复的判断的初始行,初始列如何计算获取的


总结

去重方式的不同

  • sort排序+使用used布尔数组:对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了——能实现得到唯一子集
    去重最核心的关键代码是,其实发现对于排列问题,这两种方式都能得到相同的结果;但树层上对前一位去重非常彻底,效率很高,而树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。
// 对树层中前一位去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
    continue;
}
// 要对树枝前一位去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
    continue;
}
  • 使用set:在每次for循环前创建一个set去存放for里出现过的元素,这能实现层间去重

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/762055.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

宝塔linux网站迁移步骤

网站迁移到新服务器步骤 1.宝塔网站迁移&#xff0c;有个一键迁移工具&#xff0c;参考官网 宝塔一键迁移API版本 3.0版本教程 - Linux面板 - 宝塔面板论坛 (bt.cn)2 2.修改域名解析为新ip 3.如果网站没有域名&#xff0c;而是用ip访问的&#xff0c;则新宝塔数据库的wp_o…

mysql主键自增连续新增时报错主键重复-Duplicate entry “x” for key PRIMARY

mysql主键自增连续新增时报错主键重复 1、mysql数据库设置数据库主键自增的规律 id -- AUTO_INCREMENT2、可视化工具查看自增没问题 3、问题描述 新增第一个时操作成功&#xff0c;新增第二个时候操作失败报错&#xff1a; Duplicate entry “x” for key PRIMARY4、分析&a…

[BUUCTF从零单排] Web方向 02.Web入门篇之『常见的搜集』解题思路(dirsearch工具详解)

这是作者新开的一个专栏《BUUCTF从零单排》&#xff0c;旨在从零学习CTF知识&#xff0c;方便更多初学者了解各种类型的安全题目&#xff0c;后续分享一定程度会对不同类型的题目进行总结&#xff0c;并结合CTF书籍和真实案例实践&#xff0c;希望对您有所帮助。当然&#xff0…

手把手教你考下39张免费亚马逊AWS证书和学习徽章

小李哥目前共考了39项亚马逊云(AWS)徽章&#xff0c;这也是普通用户可考的全部徽章。这篇文章会介绍如何报名、复习、通过这39张徽章提升云计算基本技能&#xff0c;了解全球第一大云厂亚马逊云科技前沿技术。这篇文章在领英爆&#x1f525;&#xff0c;有将近100k浏览量和11k的…

Linux:系统安全及应用

目录 一、系统账号管理 1.1、系统账号清理 1.2、密码安全控制 1.3、命令历史限制 二、限制su命令用户 三、PAM安全认证 四、sudo机制提升权限 4.1、sudo机制介绍 4.2、用户别名案例 4.3、启用sudo操作日志 4.4、其他案列sudo 4.5、开关机安全控制 4.6、限制更改GR…

root密码忘了怎么办(从系统引导过程解决)

目录 1.Linux系统密码忘记 2.系统引导过程 2.1 systemd 2.2 GRUB和GRUB2 2.3 运行级别 3.修复MBR扇区故障和GRUB引导故障 3.1 MBR扇区故障 3.2 GRUB引导故障 1.Linux系统密码忘记 我们在生活中经常遇到这类困扰&#xff0c;就是某个账号还是账户密码忘了&#xff0c;这…

Docker 部署 Nacos v2.3.2 版本

文章目录 Github官网文档Nacos 生态图Nacos Dockerdocker-compose.ymlapplication.propertiesNacos 官方示例 Github https://github.com/alibaba/nacos 官网 https://nacos.io/ 文档 https://nacos.io/docs/latest/what-is-nacos/ Nacos 生态图 Nacos Docker 镜像&…

《信创数据库沙龙上海站:共话发展,智启未来》

2024 年 6 月 29 日周六 14:00&#xff0c;信创数据库沙龙在上海市徐汇区建国西路 285 号科投大厦 13 楼金星厅成功举办。本次活动吸引了众多学术界和产业界的专家、学者以及技术爱好者参与。 活动中&#xff0c;多位嘉宾带来了精彩分享。薛晓刚探讨了 Oracle 在国内的前景&a…

Java全套智慧校园系统源码:微信小程序+电子班牌 让教育更智能化的一套数字化校园管理系统源码

Java全套智慧校园系统源码&#xff1a;微信小程序电子班牌 让教育更智能化的一套数字化校园管理系统源码 智慧校园管理系统是一种利用科技手段优化学校教育和管理的平台。它可以涵盖多个方面&#xff0c;例如教学、管理、服务等。其中包括智能化教室、智慧校园卡、校园安全监控…

基于flask的闪现、g对象、蓝图

【 一 】闪现&#xff08;flash&#xff09; # 1 flask中得闪现存放数据的地方&#xff0c;一旦取了&#xff0c;数据就没了-实现跨请求间传递数据 # 2 django中有没有类似的东西&#xff1f;message 消息框架# 3 基本使用1 设置&#xff1a;flash(欢迎你、欢迎来到澳门赌场&a…

Dns被莫名篡改的问题定位(笔记)

引言&#xff1a;最近发现用户的多台机器上出现了Dns被莫名修改的问题&#xff0c;从系统事件上看并未能正常确定到是那个具体软件所为&#xff0c;现在的需求就是确定和定位哪个软件具体所为。 解决思路&#xff1a; 首先到IPv4设置页面对Dns进行设置&#xff1a;通过ProcExp…

昇思25天学习打卡营第8天|MindSpore-SSD目标检测

SSD目标检测介绍 SSD,全称Single Shot MultiBox Detector,是Wei Liu在ECCV 2016上提出的一种目标检测算法。使用Nvidia Titan X在VOC 2007测试集上,SSD对于输入尺寸300x300的网络,达到74.3%mAP(mean Average Precision)以及59FPS;对于512x512的网络,达到了76.9%mAP ,超…

短视频电商源码怎么选择

随着移动互联网的迅猛发展&#xff0c;短视频电商成为了一种热门的商业模式。很多商家和创业者都希望能够快速搭建一个短视频电商平台来推广和销售自己的产品。然而&#xff0c;选择合适的短视频电商源码并不是一件容易的事情。在选择之前&#xff0c;有一些关键因素需要考虑。…

STC8/32 软硬件I2C通讯方式扫描I2C设备地址

STC8/32 软硬件I2C通讯方式扫描I2C设备地址 📄主要用于检测挂载在I2C总线上的设备。在驱动I2C设备之前,如果能扫描到该设备,说明通讯设备可以连接的上,在提前未知I2C地址的情况下,可以方便后面的驱动代码的完善。 🔬扫描测试效果:(测试mpu6050以及ssd1306 i2c oled )…

本科学历|艺术创业公司经理限定美国西部访问学者申请成功

U经理属于自费访学&#xff0c;本科学历&#xff0c;无文章及课题&#xff0c;但有较丰富的艺术创意及艺术教育实际操作经验&#xff0c;要求申美国西部地区的学校。最终我们为其获得俄勒冈州立大学访问学者邀请函。之前拟定的申请设想全部实现&#xff1a;西部地区、专业契合、…

【Lua小知识】Vscode中Emmylua插件大量报错的解决方法

起因 Vscode写Lua用的好好的&#xff0c;最近突然出现了大量报错。 看报错是有未定义的全局变量&#xff0c;这里查日志才发现是由于0.7.5版本新增诊断启用配置&#xff0c;所以导致了原先好的代码&#xff0c;现在出现了大量的报错。 解决方案一 最直接的方法当然是在配置中直…

【单片机毕业设计选题24040】-基于STM32的蓝牙防丢器设计

系统功能: 系统上电后显示“欢迎使用蓝牙防丢系统请稍后”两秒钟显示正常界面&#xff0c;如果蓝牙正常连接OLED显示Connected, 蓝牙未连接则显示DisConnected同时蜂鸣器报警 蓝牙正常连接后在APP上每隔三秒显示一个Connected 系统功能框图: 主要功能模块原理图: 电源时钟…

CSS|04 复合选择器伪类选择器属性选择器美化超链接

基本选择器&#xff1a;见上篇基本选择器 复合选择器选择器1,选择器2{属性:值;} 多元素选择器&#xff0c;同时匹配选择器1和选择器2&#xff0c;多个选择器之间用逗号分隔举例&#xff1a; p,h1,h2{margin:0px;}E F{属性:值;} 后代元素选择器&#xff0c;匹配所有属于E元素后…

【Python机器学习】模型评估与改进——分组交叉验证

分组交叉验证是非常常见的一种交叉验证策略&#xff0c;它适用于数据中的分组高度相关时。比如我们想构建一个从人脸图片中识别情感的系统&#xff0c;并且收集了100个人的照片的数据集&#xff0c;其中每个人都进行了多次拍摄&#xff0c;分别展示了不同的情感。我们的目标是构…

Python 文件操作

文件编码 将文件的内容翻译为二进制 文件操作 打开文件 open函数 语法&#xff1a; open(name, mode, encoding)name&#xff1a;文件名的字符串&#xff0c;可以包含具体路径。若没有路径&#xff0c;则默认为与py文件位于同一层 mode&#xff1a;打开文件的模式&#xf…
最新文章