深入理解回溯算法

大家好,我是 方圆,本篇我们来讲回溯。回溯相当于穷举搜索,它会尝试各种可能的情况直到找到一个满足约束条件的解,寻找解的手段一般通过 DFS 实现,是一个 增量构造答案 的过程。回溯法适用于解决能够将原问题拆分成子问题的题目,以构造长为 n 的字符串为例进行讲解:

在构造长为 n 的字符串时,从可选择的字符中选取一个字符,这样就构造出了长为 1 的字符串,那么接下来便需要构造长 n - 1 的字符串,再选取一个字符,便构造除了长为 2 的字符串,那么接下来需要构造长为 n - 2 的字符串,以此类推,过程如下图所示:
在这里插入图片描述
这样不断地解决子问题,直到满足条件,得到问题的解的过程,便是对回溯法的应用。在这个过程中,我们需要考虑如下三个要点:

  1. 当前问题:即例子中的构造长为 n 的字符串
  2. 每一步的操作 :即例子中在每一步中的“枚举字母”
  3. 子问题:即例子中的构造长为 n - 1 的字符串

根据这三个要点,将其写成 Java 的回溯代码,如下:

    // 定义全局变量记录结果值
    List<String> res;

    int n;

    /**
     * 回溯法构造长为 n 的字符串
     *
     * @param selected 选择列表:路径元素的取值范围
     * @param path     走过的路径
     */
    private void backtrack(char[] selected, StringBuilder path) {
        // 结束条件(构造长为 n 的字符串)
        if (n == path.length()) {
            res.add(path.toString());
            return;
        }

        // 每一步的操作:在选择列表中,枚举字母,构造字符串
        for (char c : selected) {
            path.append(c);
            // 子问题:构造长为 n - 1 的字符串
            backtrack(selected, path);
            // 恢复现场
            path.deleteCharAt(path.length() - 1);
        }
    }

void backtrack(char[] selected, StringBuilder path) 方法中,定义 char[] selected“选择列表”,表示的是构造“路径”时的 决策范围,路径中的元素需要从该对象中选取;定义 StringBuilder path“路径” ,表示递归过程中已经做过的选择;每次递归完成时,通常都需要 “恢复现场” 的操作,即将走过的路径恢复到递归之前;定义边界条件,当路径满足该条件时,即可记录该路径为答案(之一)。

在解决回溯问题时,需要先考虑当前问题、每一步的操作和子问题,再根据这三个要点定义回溯方法。下面我们通过对子集型回溯、组合型回溯和排列型回溯对回溯问题进行学习和总结。

子集型回溯

子集型回溯的问题,对于 每个元素都可以选或不选,我们以 78. 子集 中等 为例,看看它该如何求解:

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

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

示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:
输入:nums = [0]
输出:[[],[0]]

提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同

根据题意,可知构造子集时每个元素都可以选或不选,接下来便要考虑解题时的三个要点:

  1. 当前问题:从下标大于等于 i 的子数组中构造子集
  2. 每一步的操作 :将下标大于等于 i 的元素加入到路径中
  3. 子问题:题目要求不能有重复的子集,所以子问题要从下标大于等于 i + 1 的子数组中构造子集

题解如下,注意关注其中的注释信息:

public class Solution78 {

    // 定义全局变量
    List<List<Integer>> res;

    public List<List<Integer>> subsets(int[] nums) {
        res = new LinkedList<>();
        backtrack(nums, 0, new LinkedList<>());
        return res;
    }

    /**
     * 回溯
     *
     * @param nums 选择列表
     * @param begin 构造子集开始的子数组索引
     * @param path 路径
     */
    private void backtrack(int[] nums, int begin, LinkedList<Integer> path) {
        // 走过的所有路径均为答案之一
        res.add((List<Integer>) path.clone());
        for (int i = begin; i < nums.length; i++) {
            // 每一步的操作:将下边大于等于 begin 的元素加入到路径中
            path.add(nums[i]);
            // 子问题:
            backtrack(nums, i + 1, path);
            // 恢复现场:即将递归前加入路径的元素移除
            path.removeLast();
        }
    }
}

接下来我们再看一道 131. 分割回文串 中等:

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串。返回 s 所有可能的分割方案。

示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]

示例 2:
输入:s = “a”
输出:[[“a”]]

提示:
1 <= s.length <= 16
s 仅由小写英文字母组成

根据题意,字符串所有子串必须都是回文串,而对于字符串中的每个字符元素,我们需要根据它是否能构成子串来判断它的选或不选,所以依然是子集型回溯。接下来,需要考虑下三个要点:

  1. 当前问题:题目要求分割方案中的所有子串都为回文串,那么当前问题便是从大于等于下标 i 的子数组中判断并构造回文串集合
  2. 每一步的操作:判断是否为回文串,为回文串的话加入到路径中
  3. 子问题:从大于等于下标 i + 1 的子数组中判断并构造回文串集合
public class Solution131 {

    // 定义全局变量
    List<List<String>> res;

    public List<List<String>> partition(String s) {
        res = new LinkedList<>();
        backtrack(s, 0, new LinkedList<>());
        return res;
    }

    private void backtrack(String s, int begin, LinkedList<String> path) {
        // 所有子串均为回文串,添加答案并结束
        if (begin == s.length()) {
            res.add((List<String>) path.clone());
            return;
        }

        for (int i = begin; i < s.length(); i++) {
            String cur = s.substring(begin, i + 1);
            // 每一步的操作:判断是否为回文串,是的话加入路径中,并继续处理子问题
            if (isReverse(cur)) {
                path.add(cur);
                // 子问题:从大于小于等于下标 i + 1 的子数组中判断并构造回文串集合
                backtrack(s, i + 1, path);
                // 恢复现场
                path.removeLast();
            }
        }
    }

    private boolean isReverse(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
相关练习
  • 257. 二叉树的所有路径 简单
  • 17. 电话号码的字母组合 中等
  • 784. 字母大小写全排列 中等
  • 22. 括号生成 中等

组合型回溯

组合型回溯与子集型回溯相似,它同样也会涉及元素的选与不选,不同的是组合型回溯需要 增加判断条件 来满足题意,达到 剪枝优化 的目的,以 77. 组合 中等 为例,看一下该如何解决:

给定两个整数 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]]

提示:
1 <= n <= 20
1 <= k <= n

它限定了路径长度为 n,而在子集型回溯中是不会包含这个限制的,除了要考虑解决回溯问题的三个要点外,组合型回溯还需要考虑 剪枝优化 条件:

  1. 当前问题:从小于等于 n 的范围内,在路径中添加第 i 个元素
  2. 每一步的操作:加入当前元素或不加入当前元素
  3. 子问题:从小于等于 n - 1 的范围内,在路径中添加第 i + 1 个元素
  4. 剪枝优化:路径中的元素为 k 个时;路径中的元素加上可选择列表中的剩余的元素不足 k 个时;n 取值小于等于 0 时

题解如下:

public class Solution77 {

    List<List<Integer>> res;
    int k;

    public List<List<Integer>> combine(int n, int k) {
        res = new LinkedList<>();
        this.k = k;
        backtrack(n, new LinkedList<>());
        return res;
    }

    // 1. 当前问题:从小于等于 n 的范围内,在路径中添加第 i 个元素
    // 2. 每一步的操作:加入当前元素或不加入当前元素
    // 3. 子问题:从小于等于 n - 1 的范围内,在路径中添加第 i + 1 个元素
    // 4. 剪枝优化:路径中的元素为 k 个时;路径中的元素加上可选择列表中的剩余的元素不足 k 个时;n 取值小于等于 0 时
    private void backtrack(int n, LinkedList<Integer> path) {
        // 剪枝优化
        if (path.size() == k) {
            res.add((List<Integer>) path.clone());
            return;
        }
        if (n + path.size() < k) {
            return;
        }
        if (n <= 0) {
            return;
        }

        // 加
        path.add(n);
        backtrack(n - 1,  path);
        // 加入过元素后需要恢复现场
        path.removeLast();
        // 不加
        backtrack(n - 1, path);
    }
}

接下来,再看一题 216. 组合总和 III 中等,同样地,它限制了组合长度为 k:

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

只使用数字1到9
每个数字 最多使用 一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

提示:
2 <= k <= 9
1 <= n <= 60

考虑组合型回溯的四个要点:

  1. 当前问题:在小于等于 n 的条件下,在路径中添加第 i 个元素
  2. 每一步的操作:添加当前值或不添加当前值
  3. 子问题:在小于等于 n - 1 的条件下,在路径中添加第 i + 1 个元素
  4. 剪枝优化:元素大于等于 k 个;元素和大于等于 n;num 取值小于等于 0
class Solution216 { 

    List<List<Integer>> res;

    int n;

    int k;

    public List<List<Integer>> combinationSum3(int k, int n) {
        this.res = new LinkedList<>();
        this.k = k;
        this.n = n;
        backtrack(9, new LinkedList<>(), 0);
        return res;
    }

    // 1. 当前问题:在小于等于 n 的条件下,在路径中添加第 i 个元素
    // 2. 每一步的操作:添加当前值或不添加当前值
    // 3. 子问题:在小于等于 n - 1 的条件下,在路径中添加第 i + 1 个元素
    // 4. 剪枝优化:元素大于等于 k 个;元素和大于等于 n;num 取值小于等于 0
    private void backtrack(int num, LinkedList<Integer> path, int sum) {
        if (path.size() == k && sum == n) {
            res.add((List<Integer>) path.clone());
            return;
        }
        if (path.size() >= k) {
            return;
        }
        if (sum > n) {
            return;
        }
        if (num <= 0) {
            return;
        }

        // 添加
        path.add(num);
        backtrack(num - 1, path, sum + num);
        path.removeLast();
        // 不添加
        backtrack(num - 1, path, sum);
    }
}
相关练习
  • 39. 组合总和 中等
  • 40. 组合总和 II 中等

排列型回溯

排列型相比于组合型,对于不同元素的排列顺序是有区别的,比如在排列型回溯中,[1, 2][2, 1] 是两种 不同的 排列,而在组合中,它们是相同的组合。求解排列型回溯问题时,一般会使用 boolean visited[] 数组来标记对应下标处的元素有没有被选择过,以此来判断哪些元素是能选的,哪些元素是不能选的。下面我们以 46. 全排列 中等 为例,看看该如何求解:

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

示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:
输入:nums = [1]
输出:[[1]]

提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同

首先我们需要考虑下解决回溯问题的三个要点:

  1. 当前问题:向路径中添加第 i 个元素
  2. 每一步的操作:在路径中添加未被选择过的元素
  3. 子问题:向路径中添加第 i + 1 个元素
public class Solution46 {

    List<List<Integer>> res;

    public List<List<Integer>> permute(int[] nums) {
        res = new LinkedList<>();
        backtrack(nums, new boolean[nums.length], new LinkedList<>());
        return res;
    }

    // 1. 当前问题:向路径中添加第 i 个元素
    // 2. 每一步的操作:在路径中添加未被选择过的元素
    // 3. 子问题:向路径中添加第 i + 1 个元素
    private void backtrack(int[] nums, boolean[] visited, LinkedList<Integer> path) {
        // 结束条件
        if (path.size() == nums.length) {
            res.add((List<Integer>) path.clone());
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (visited[i]) {
                continue;
            }
            path.add(nums[i]);
            visited[i] = true;
            backtrack(nums, visited, path);
            // 恢复现场
            path.removeLast();
            visited[i] = false;
        }
    }
}
相关练习
  • 47. 全排列 II 中等
  • LCR 157. 套餐内商品的排列顺序 中等
  • 面试题 08.12. 八皇后 困难

回溯算法使用的注意点

注意使用回溯法时需要关注题目中是否要求返回所有路径(组合),如果不需要的话,可以考虑使用动态规划或其他方法,如题目 377. 组合总和 Ⅳ 中等,该题并不要求返回所有组合,而是组合数目。


巨人的肩膀

  • Bilibili - 回溯算法套路①子集型回溯【基础算法精讲 14】
  • Bilibili - 回溯算法套路②组合型回溯+剪枝【基础算法精讲 15】
  • Bilibili - 回溯算法套路③排列型回溯+N皇后【基础算法精讲 16】

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

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

相关文章

OpenSSL实现AES-CBC加解密,可一次性加解密任意长度的明文字符串或字节流(QT C++环境)

本篇博文讲述如何在Qt C的环境中使用OpenSSL实现AES-CBC-Pkcs7加/解密&#xff0c;可以一次性加解密一个任意长度的明文字符串或者字节流&#xff0c;但不适合分段读取加解密的&#xff08;例如&#xff0c;一个4GB的大型文件需要加解密&#xff0c;要分段读取&#xff0c;每次…

常见通信协议

1、串口&#xff1a;&#xff08;串行异步全双工&#xff0c;先发低位&#xff09; 因为是异步的&#xff0c;所以没有时钟线&#xff0c;因为是全双工&#xff0c;所以有两条数据传输线&#xff0c;实现数据的收发。 帧格式 起始位1位&#xff0c;数据位8位&#xff0c;校验…

【C++】stack、queue和priority_queue的模拟实现

在本篇博客中&#xff0c;作者将会讲解STL中的stack、queue和priority_queue的模拟实现&#xff0c;同时还会带大家了解一下deque这个容器。 一.什么是适配器 STL中一共有6大组件&#xff1a;容器&#xff0c;适配器&#xff0c;空间配置器&#xff0c;仿函数&#xff0c;迭代器…

控制台调试 hover 后才出现的元素

调试 hover后才出现的元素 打开开发者工具&#xff0c;鼠标放在hover时才出现的元素上&#xff0c;然后点击右键&#xff1b; 不要选中任何选项&#xff0c;将鼠标移动到开发者工具的调试面板中&#xff1b; 按下N键&#xff0c;此时悬浮的元素不会消失&#xff0c;定位成功。…

Java注解介绍

注解&#xff08;Annotation&#xff09;是Java提供的一种元数据形式&#xff0c;它可以被添加到Java代码的各种元素上&#xff0c;如类、方法、变量、参数等。注解的作用主要包括&#xff1a; 1. 代码文档&#xff1a;注解可以用于生成文档&#xff0c;提高代码的可读性。 2.…

前端之深拷贝

前提&#xff1a; 就是在实际开发中&#xff0c;我有一个编辑的弹窗&#xff0c;可以查看和编辑&#xff0c;因为弹窗里面是一个步骤条&#xff0c;点击下一步就要向对应的接口发送请求&#xff0c;考虑到就比如我点击下一步&#xff0c;此次表箱信息其实不需要修改&#xff0…

大模型_DISC-MedLLM基于Baichuan-13B-Base医疗健康对话

文章目录 DISC-MedLLM介绍概述数据集部署推理流程 DISC-MedLLM 介绍 DISC-MedLLM 是一个专门针对医疗健康对话式场景而设计的医疗领域大模型&#xff0c;由复旦大学数据智能与社会计算实验室 (Fudan-DISC) 开发并开源。 该项目包含下列开源资源: DISC-Med-SFT 数据集 (不包…

智慧园区综合物业管理平台解决方案PPT(130页精品)

我们对智慧园区的理解 智慧园区&#xff0c;是通过信息技术和各类资源的整合&#xff0c;充分降低企业运营成本&#xff0c;提高工作效率&#xff0c;加强各类园区创新、服务和管理能力&#xff0c;为园区铸就一套超强的软实力。智慧园区的实现是多技术融合、多系统融合、多领域…

初识C语言——第十三天

关键字2&#xff1a; static 修饰局部变量&#xff0c;改变了局部变量的生命周期&#xff08;本质上是改变了变量的存储类型&#xff09; static修饰全局变量&#xff0c;使得这个全局变量只能在自己所在的源文件&#xff08;.c)内部可以使用&#xff0c;其他源文件不能使用 …

全方位了解 Meta Llama 3

本文将为您提供 Llama 3 的全面概览&#xff0c;从其架构、性能到未来的发展方向&#xff0c;让您一文了解这一革命性大语言模型的所有要点。 Meta Llama 发展历程 Llama 1 Llama 是由 Meta(FaceBook) AI 发布的一个开源项目&#xff0c;允许商用&#xff0c;影响力巨大。Lla…

基于springboot+vue+Mysql的在线动漫信息平台

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

Qt | QLCDNumber 类(LCD 数字),LCD 表示液晶显示屏

01、上节回顾 Qt 基础教程合集02、QLCDNumber 1、QLCDNumber 类用于显示类似于 LCD 显示屏上的字符(见右图) ​ 2、QLCDNumber 类是 QFrame 类的直接子类,因此 QLCDNumber 以使用从 QFrame 类继承而来的边框效果 3、QLCDNumber 可显示的符号有:0,1,2,3,4,5,6,7,8,…

ue引擎游戏开发笔记(33)——武器与角色的匹配,将新武器装备到角色身上

1.需求分析&#xff1a; 武器能出现在世界中&#xff0c;完成了第一步&#xff0c;下一步需要角色和武器适配&#xff0c;即不论角色跑动&#xff0c;射击等&#xff0c;武器和角色都相匹配&#xff0c;将武器装备到角色身上。 2.操作实现&#xff1a; 1.首先先把角色原有的武…

【数据结构】--- 深入剖析二叉树(中篇)--- 认识堆堆排序Topk

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 数据结构之旅 文章目录 &#x1f3e0; 初识堆 &#x1f4d2; 堆的概念 &#x1f4d2; 堆的性质 &#x1f3e0; 向上调整算法 && 向下调整算…

vector的oj题

1.只出现1次的数字 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题&#xff0c;且该算法只使用常量额外空间。 方法&#xff1a;…

【Stable Diffusion】三句话,让Ai帮你画18万张图

本文介绍Stable Diffusion的快速上手&#xff0c;本地部署&#xff0c;以及更多有趣的玩法展示。 在 DALL-E 2 和 Imagen 之后&#xff0c;AI绘图领域又一个热乎的深度学习模型出炉——Stable Diffusion 。8月份发布的 Stable Diffusion 更加高效且轻量&#xff0c;可以在消费…

第六节课《Lagent AgentLego 智能体应用搭建》

PDF链接&#xff1a;https://pan.baidu.com/s/1JFtvBWgEGFWJq8pHafvIUg?pwd6666 提取码&#xff1a;6666 Lagent & AgentLego 智能体应用搭建_哔哩哔哩_bilibili https://github.com/InternLM/Tutorial/blob/camp2/agent/README.md InternStudio 一、为什么需要agent…

网页html版面分析-- BeauifulSoup(python 文档解析提取)

介绍 BeauifulSoup 是一个可以从HTML或XML 文件中提取数据的python库&#xff1b;它能通过转换器实现惯用的文档导航、查找、修改文档的方式。 BeauifulSoup是一个基于re开发的解析库&#xff0c;可以提供一些强大的解析功能&#xff1b;使用BeauifulSoup 能够提高提取数据的效…

R语言Rstudio突然无法启动?如何解决

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

由于找不到msvcp120.dll,无法继续执行代码的5种解决方法

在操作计算机的过程中&#xff0c;您或许会遇到这样一种情形&#xff1a;当试图启动某个软件应用程序时&#xff0c;系统突然弹出一个错误提示框&#xff0c;明确指出“找不到msvcp120.dll”&#xff0c;它会导致程序无法正常启动或运行。为了解决这个问题&#xff0c;我总结了…