彻底搞懂回溯算法(例题详解)

news2024/11/15 19:30:25

目录

什么是回溯算法:

 子集问题:

子集问题II(元素可重复但不可复选): 

 组合问题:

组合问题II(元素可重复但不可复选):

排列问题:

排列问题II(元素可重复但不可复选):


什么是回溯算法:

「回溯是递归的副产品,只要有递归就会有回溯」,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都使用了递归。

详细地说:可以将回溯算法过程理解成一颗多叉树的遍历过程, 回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。

回溯问题的基本框架:


void backtrack(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
 
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {//注意i=0,i=start的区别
        处理节点;
        backtrack(路径,选择列表); // 递归  注意(i)和(i++)的区别  
        回溯,撤销处理结果
    }
}

多叉树的遍历:

与二叉树的遍历类似,但要遍历所有的子节点:

 public void traverse(TreeNode root){
        if(root == null){
            return ;
        }
 
        //前序遍历的位置
        for(Node child : root.children){
            traverse(child);
        }
           //后序遍历的位置
        }

 如果将前后续遍历的位置放到for循环里面,与上图的区别在于不遍历根节点(通过后面例题加深理解)

那么有了上面的一定了解后,我们看下例题,具体怎么使用框架

 子集问题:

问题描述:

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

 题目链接:78. 子集 - 力扣(LeetCode)

这是一个典型的回溯问题,首先,我们将它模拟成一颗多叉树(根节点为空):

解决这个问题的关键在于如何遍历这颗多叉树,并且子集不重复 ,在遍历的过程中,我们要将每一个节点都收录到集合中,最后返回这个集合。之后我们只需要让子集不重复就好了,这里我们可以通过设定一个变量start来记录当前走过的位置,使其不断+1来进行迭代,保证不重复,具体实现如下:

class Solution {
    //定义二维数组res用于存储结果
    List<List<Integer>> res = new LinkedList<>();
    //定义路径数组
    LinkedList<Integer> track = new LinkedList<>(); 
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums,0);
        return res;
    }
     void backtrack(int[] nums,int start){
          //添加路径数组到结果数组中
         res.add(new LinkedList<>(track));
          //for循环遍历数组nums,这里相当于遍历这颗多叉树
         for(int i = start;i < nums.length;i++){
               //做选择,将选择添加到路径数组中
             track.add(nums[i]);
              //回溯,继续向后遍历
             backtrack(nums,i + 1);
             //撤销选择,将选择从路径中删除
             track.removeLast();
         }
     }
}

子集问题II(元素可重复但不可复选): 

题目链接:90. 子集 II - 力扣(LeetCode)

输入输出样例:

这里我们以例一为例:为了区别两个 2 是不同元素,后面我们写作 nums = [1,2,2']

按照之前的思路画出子集的树形结构,显然,两条值相同的相邻树枝会产生重复:

这里,我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历: 

体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过

class Solution {

    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> track = new LinkedList<>();

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 先排序,让相同的元素靠在一起
        Arrays.sort(nums);
        backtrack(nums, 0);
        return res;
    }

    void backtrack(int[] nums, int start) {
        // 前序位置,每个节点的值都是一个子集,将他们收集
        res.add(new LinkedList<>(track));
        
        for (int i = start; i < nums.length; i++) {
            // 剪枝逻辑,值相同的相邻树枝,只遍历第一条
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
       //选择操作
            track.addLast(nums[i]);
      //回溯
            backtrack(nums, i + 1);
     //撤销选择
            track.removeLast();
        }
    }
}

 组合问题:

 题目描述:

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

输入输出:

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

题目链接:77. 组合 - 力扣(LeetCode)

还是以 nums = [1,2,3] 为例,刚才让我们求所有子集,就是把所有节点的值都收集起来;现在我们只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合

class Solution {

    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯算法的递归路径
    LinkedList<Integer> track = new LinkedList<>();

    // 主函数
    public List<List<Integer>> combine(int n, int k) {
        backtrack(1, n, k);
        return res;
    }

    void backtrack(int start, int n, int k) {
        // base case
        if (k == track.size()) {
            // 遍历到了第 k 层,收集当前节点的值
            res.add(new LinkedList<>(track));
            return;
        }
        
        // 回溯算法标准框架
        for (int i = start; i <= n; i++) {
            // 选择
            track.addLast(i);
            // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            backtrack(i + 1, n, k);
            // 撤销选择
            track.removeLast();
        }
    }
}

组合问题II(元素可重复但不可复选):

题目描述:

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

题目链接:39. 组合总和 - 力扣(LeetCode)

上述子集问题中,我们通过变量start+1来控制树的生成,也就是剪枝,但是这题可以无限制选用一个数字,那么剪枝也就没有必要了,这里我们保持start不变(树会一直生成),只需改变base case(限制条件)就行了,同时定义一个trackSum来维护路径和:

class Solution {

    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯的路径
    LinkedList<Integer> track = new LinkedList<>();
    // 记录 track 中的路径和
    int trackSum = 0;

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates.length == 0) {
            return res;
        }
        backtrack(candidates, 0, target);
        return res;
    }

    // 回溯算法主函数
    void backtrack(int[] nums, int start, int target) {
        // base case,找到目标和,记录结果
        if (trackSum == target) {
            res.add(new LinkedList<>(track));
            return;
        }
        // base case,超过目标和,停止向下遍历
        if (trackSum > target) {
            return;
        }

        // 回溯算法标准框架
        for (int i = start; i < nums.length; i++) {
            // 选择 nums[i]
            trackSum += nums[i];
            track.add(nums[i]);
            // 递归遍历下一层回溯树
            // 同一元素可重复使用,注意参数
            backtrack(nums, i, target);
            // 撤销选择 nums[i]
            trackSum -= nums[i];
            track.removeLast();
        }
    }
}

排列问题:

题目描述:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

题目链接:46. 全排列 - 力扣(LeetCode) 

刚才讲的组合/子集问题使用 start 变量保证元素 nums[start] 之后只会出现 nums[start+1..] 中的元素,通过固定元素的相对位置保证不出现重复的子集。但排列问题本身就是让你穷举元素的位置,nums[i] 之后也可以出现 nums[i] 左边的元素,所以之前的那一套玩不转了,需要额外使用 used 数组来标记哪些元素还可以被选择,这就相当于剪枝的作用。将全排列问题模拟成一颗多叉树:

class Solution {

    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯算法的递归路径
    LinkedList<Integer> track = new LinkedList<>();
    // track 中的元素会被标记为 true
    boolean[] used;

    /* 主函数,输入一组不重复的数字,返回它们的全排列 */
    public List<List<Integer>> permute(int[] nums) {
        used = new boolean[nums.length];
        backtrack(nums);
        return res;
    }

    // 回溯算法核心函数
    void backtrack(int[] nums) {
        // base case,到达叶子节点
        if (track.size() == nums.length) {
            // 收集叶子节点上的值
            res.add(new LinkedList(track));
            return;
        }

        // 回溯算法标准框架
        for (int i = 0; i < nums.length; i++) {
            // 已经存在 track 中的元素,不能重复选择
            if (used[i]) {
                continue;
            }
            // 做选择
            used[i] = true;
            track.addLast(nums[i]);
            // 进入下一层回溯树
            backtrack(nums);
            // 取消选择
            track.removeLast();
            used[i] = false;
        }
    }
}

排列问题II(元素可重复但不可复选):

比如输入 nums = [1,2,3],那么这种条件下的全排列共有 3^3 = 27 种:

[
  [1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3],
  [2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3],
  [3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3]
]

标准的全排列算法利用 used 数组进行剪枝,避免重复使用同一个元素。如果允许重复使用元素的话,直接放飞自我,去除所有 used 数组的剪枝逻辑就行了

代码详解:

class Solution {

    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> track = new LinkedList<>();

    public List<List<Integer>> permuteRepeat(int[] nums) {
        backtrack(nums);
        return res;
    }

    // 回溯算法核心函数
    void backtrack(int[] nums) {
        // base case,到达叶子节点
        if (track.size() == nums.length) {
            // 收集叶子节点上的值
            res.add(new LinkedList(track));
            return;
        }

        // 回溯算法标准框架
        for (int i = 0; i < nums.length; i++) {
            // 做选择
            track.add(nums[i]);
            // 进入下一层回溯树
            backtrack(nums);
            // 取消选择
            track.removeLast();
        }
    }
}

最后总结:

回溯算法本质就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:


void backtrack(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
 
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {//注意i=0,i=start的区别
        处理节点;
        backtrack(路径,选择列表); // 递归  注意(i)和(i++)的区别  
        回溯,撤销处理结果
    }
}

写 backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。 最后返回结果集合,得到答案。

结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固自己的知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+关注+收藏,你们的鼓励是我创作的最大动力!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全球十大正规伦敦金交易平台app软件最新排名(综合版)

伦敦金作为当前国际市场中较为成熟、灵活的投资产品自然备受青睐&#xff0c;但投资者在选择交易软件时&#xff0c;应该尽量选择在行业内排名较高&#xff0c;口碑较好的平台&#xff0c;这样才能获得可靠的投资服务。刚开始不太懂得如何选择伦敦金软件的时候&#xff0c;投资…

SpringBoot原理-配置优先级(黑马学习笔记)

配置优先级 在我们前面的课程当中&#xff0c;我们已经讲解了SpringBoot项目当中支持的三类配置文件&#xff1a; ● application.properties ● application.yml ● application.yaml 在SpringBoot项目当中&#xff0c;我们要想配置一个属性&#xff0c;可以通过这三种方…

手写模拟器,解放双手!效果炸裂的生产工具

手写模拟器是一款基于Handright的仿手写图片生成软件&#xff0c;可以让你的电脑和手机也能写出漂亮的手写字&#xff0c;你只需要输入你想要写的内容&#xff0c;选择你喜欢的字体和背景&#xff0c;就可以生成一张高仿真的手写图片&#xff0c;用于各种场合&#xff0c;比如做…

搜索算法(算法竞赛、蓝桥杯)--双向BFS双向奔赴

1、B站视频链接&#xff1a;B18 双向BFS Nightmare_哔哩哔哩_bilibili 题目链接&#xff1a;Problem - 3085 #include <bits/stdc.h> using namespace std; const int N810; int n,m; #define x first #define y second char g[N][N]; //地图 int vis[N][N]; //2表示女…

3.2日学习打卡----初学FastDFS(二)

3.2日学习打卡 目录: 3.2日学习打卡SpringBoot整合FastDFS实战开发文件上传 FastDFS集成Nginx环境搭建 SpringBoot整合FastDFS 由GitHub大牛tobato在原作者YuQing与yuqih发布的JAVA客户端基础上进行了大量重构工作&#xff0c;并于GitHub上发布了FastDFS-Client1.26.5。 主要特…

Java字符串相关类的底层原理

Java字符串相关类的底层原理

MySQL(2/3)

select和别名的使用 主要是用以查询数据 语法&#xff1a;select 字段 from 库名 -- *代表全部字段 select * from student; -- 可以查询多个字段&#xff0c;并使用as起别名&#xff0c;as可以省略 select id as bbb ,name as hhh from student; -- 可以使用函数concat(a,b…

Matlab|【免费】基于合作博弈的综合能源系统利益分配优化调度

目录 主要内容 部分代码 结果一览 下载链接 主要内容 该程序实现的模型为综合能源系统利益分配优化调度&#xff0c;采用合作博弈方法&#xff0c;模型针对IES系统的P2G、电解槽、甲烷反应器、储氢罐、CHP和燃气锅炉等设备进行建模&#xff0c;实现基于合作博弈的…

Stable Cascade-ComfyUI中文生图、图生图、多图融合基础工作流分享

最近 ComfyUI对于Stable Cascade的支持越来越好了一些&#xff0c;官方也放出来一些工作流供参考。 这里简单分享几个比较常用的基础工作流。 &#xff08;如果还没有下载模型&#xff0c;可以先阅读上一篇Stable Cascade升级&#xff0c;现在只需要两个模型&#xff09; &a…

把Anaconda添加进环境变量的方法(解决pip识别不到环境的问题)

找到你的Anaconda的安装根目录 比如我的是在&#xff1a;C:\ProgramData\Anaconda3 那么只需要将以下目录添加进环境变量即可&#xff1a; C:\ProgramData\Anaconda3C:\ProgramData\Anaconda3\ScriptsC:\ProgramData\Anaconda3\Library\binC:\ProgramData\Anaconda3\condabin…

volatile 关键字 (一)

volatile 关键字 &#xff08;一&#xff09; 文章目录 volatile 关键字 &#xff08;一&#xff09;如何保证变量的可见性&#xff1f;如何禁止指令重排序&#xff1f; 文章来自Java Guide 用于学习如有侵权&#xff0c;立即删除 如何保证变量的可见性&#xff1f; 在 Java 中…

云时代【7】—— 存储卷

云时代【7】—— 存储卷 四、Docker&#xff08;四&#xff09;存储卷1. 存储卷&#xff08;1&#xff09;定义&#xff08;2&#xff09;分类 2. 相关指令&#xff08;1&#xff09;管理卷 VolumeA. 创建方式方式一&#xff1a;docker volume方式二&#xff1a;docker run -v …

NFT Insider #121:苏富比去年 NFT 和数字艺术品销售额超过 3000 万美元,较 2022 年增长 50%

引言&#xff1a;NFT Insider由NFT收藏组织WHALE Members &#xff08;https://twitter.com/WHALEMembers&#xff09;、BeepCrypto &#xff08;https://twitter.com/beep_crypto&#xff09;联合出品&#xff0c;浓缩每周NFT新闻&#xff0c;为大家带来关于NFT最全面、最新鲜…

【CSS】CSS简介,CSS基础选择器详解

目录 css简介 css语法规范 css代码风格&#xff1a; css选择器的作用 css基础选择器 标签选择器 类选择器 类选择器---多类名 id选择器 id选择器和类选择器的区别&#xff1a; 通配符选择器 总结 ⭐css简介 CSS 是层叠样式表 ( Cascading Style Sheets ) 的简称,也…

机器学习-4

文章目录 前言数组创建切片索引索引遍历切片编程练习 总结 前言 本篇将介绍数据处理 Numpy 库的一些基本使用技巧&#xff0c;主要内容包括 Numpy 数组的创建、切片与索引、基本运算、堆叠等等。 数组创建 在 Python 中创建数组有许多的方法&#xff0c;这里我们使用 Numpy 中…

使用 Docker 部署 Answer 问答平台

1&#xff09;介绍 GitHub&#xff1a;https://github.com/apache/incubator-answer Answer 问答社区是在线平台&#xff0c;让用户提出问题并获得回答。用户可以发布问题并得到其他用户的详细答案、建议或信息。回答可以投票或评分&#xff0c;有助于确定有用的内容。标签和分…

Redis 群集部署

1.关系型数据库 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型基础上&#xff0c;-般面向记录。它借助于集合代数等数学概念和方法来处理数据库中的数据。关系模型指二维表格模型,因而一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织。现实世界中…

软件分层(数据结构/软件逻辑上分层+举例),相连节点的概念+如何相连,为什么是层状结构(软件分层,网络协议分层+梳理协议顺序),协议分层(打电话例子)

目录 软件分层 介绍 举例 类的继承 虚拟文件系统 线程接口封装 虚拟地址空间 总结 为什么是层状的 软件分层 网络协议 原因 梳理协议顺序 相连节点 协议分层 引入 示例 实际上 逻辑上 制定出协议 软件分层 介绍 通过将软件系统划分为不同的层次,每一层都有…

OpenHarmony、HarmonyOS打开编辑 PDF 等操作的三方组件使用教程

项目场景: 随着数字化时代的发展,PDF 文档成为广泛应用于各行业的重要文件格式。为了提高OpenHarmony/HarmonyOS生态系统的功能性和用户体验,我们需要一款支持打开、编辑PDF文件的应用程序。 使用户能够轻松打开、浏览和编辑PDF文件。该应用将充分利用OpenHarmony/HarmonyO…

Day20-磁盘管理

Day20-磁盘管理 1. cut 切:2. 磁盘历史和内外部物理结构介绍2.1 磁盘发展趋势和实现措施2.2 磁盘知识的体系结构2.3 机械磁盘的外部结构2.4 SSD固态硬盘的外部结构2.5 固态硬盘内部结构2.6 缓存在服务器各硬件上的速度和大小对比另类维度图解&#xff0c;从上到下由高速到低速&…