深入理解回溯算法

news2024/9/23 3:26:40

大家好,我是 方圆,本篇我们来讲回溯。回溯相当于穷举搜索,它会尝试各种可能的情况直到找到一个满足约束条件的解,寻找解的手段一般通过 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】

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

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

相关文章

ABAP开发(5)字符串操作

文章目录 1、CONCATENATE2、SPLIT3、SEARCH4、REPLACE 1、CONCATENATE 使用关键字CONCATENATE可以将多个字符串进行连接&#xff0c;也可以在连接的过程中添加分隔符。 2、SPLIT 3、SEARCH 4、REPLACE

wordpress外贸建站公司歪建站新版网站上线

wordpress外贸建站公司 歪猫建站 歪猫WordPress外贸建站&#xff0c;专业从事WordPress多语言外贸小语种网站建设与外贸网站海个推广、Google SEO搜索引擎优化等服务。 https://www.waimaoyes.com/dongguan

segformer部分错误

亲测有用 1、TypeError: FormatCode() got an unexpected keyword argument ‘verify‘ mmcv中出现TypeError: FormatCode() got an unexpected keyword argument ‘verify‘-CSDN博客 pip install yapf0.40.0 2、“EncoderDecoder: ‘mit_b1 is not in the backbone regist…

10 华三vlan技术介绍

AI 解析 -Kimi-ai Kimi.ai - 帮你看更大的世界 (moonshot.cn) 虚拟局域网&#xff08;VLAN&#xff09;技术是一种在物理网络基础上创建多个逻辑网络的技术。它允许网络管理员将一个物理网络分割成多个虚拟的局域网&#xff0c;这些局域网在逻辑上是隔离的&#xff0c;但实际…

SOL链DApp智能合约代币质押挖矿分红系统开发

随着区块链技术的不断发展和普及&#xff0c;越来越多的项目开始探索基于区块链的去中心化应用&#xff08;DApp&#xff09;。Solana&#xff08;SOL&#xff09;作为一条高性能、低成本的区块链网络&#xff0c;吸引了众多开发者和项目&#xff0c;其中包括了各种类型的DApp&…

昂科烧录器支持O2Micro凹凸科技的电池组管理IC OZ7708

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中O2Micro凹凸科技的电池组管理IC OZ7708已经被昂科的通用烧录平台AP8000所支持。 OZ7708是一款高度集成、低成本的电池组管理IC&#xff0c;适用于5~8s Li-Ion/Polymer电池组&a…

为什么要学Python?学Python有什么用?

为什么要学Python&#xff1f;学Python有什么用&#xff1f; 在当今的数字化时代&#xff0c;编程已成为一项宝贵的技能。Python&#xff0c;作为一种流行的编程语言&#xff0c;因其易于学习和强大的功能而受到全球开发者的青睐。本文将探讨学习Python的原因和它的实际应用&am…

武汉星起航:跨境电商领域国际竞争力卓越,引领行业再上新台阶

在全球化浪潮的推动下&#xff0c;跨境电商行业日益成为各国经济交流与合作的重要桥梁。武汉星起航电子商务有限公司&#xff0c;作为跨境电商领域的佼佼者&#xff0c;凭借其深厚的行业经验和前瞻性的战略视野&#xff0c;在国际市场上展现出强大的竞争力&#xff0c;为行业的…

【SpringMVC 】什么是SpringMVC(二)?如何整合ssm框架以及使用mybatisPlus?

文章目录 SpringMVC第三章1、ssm整合1、基本步骤1-3步4-5步6步7步8-12步13步14-15步2、添加数据3、删除数据4、配置事务5、修改数据2、pageHelpe分页1、基本步骤第四章1、mybatisPlus1、基本步骤1-45-7892、基本方法的使用查询2、新ssm项目1、基本步骤1-5678-910-111213-15Spri…

C++11,{}初始化,initializer_list,decltype,右值引用,类和对象的补充

c98是C标准委员会成立第一年的C标准&#xff0c;C的第一次更新是C03&#xff0c;但由于C03基本上是对C98缺陷的修正&#xff0c;所以一般把C98与C03合并起来&#xff0c;叫做C98/03&#xff1b; 后来原本C委员会更新的速度预计是5年更新一次&#xff0c;但由于C标准委员会的进…

2×24.5W、内置 DSP、低失真、高信噪比、I2S 输入 D 类音频功率放大器,完美替换TPA5805,晶豪,致盛,

ANT3825 是一款高集成度、高效率的双通道数字 输入功放。供电电压范围在 5V&#xff5e;18V&#xff0c;数字接口 电源支持 3.3V 或 1.8V。双通道 BTL 模式下输出 功率可以到 224.5W(4Ω&#xff0c;16V&#xff0c;THDN1%)&#xff0c; 单通道 PBTL 模式下可以输出 37W&#x…

软件测试产品交付包括哪些内容?

软件测试产品交付通常会包括以下内容: 1. 测试计划:详细的测试方案、测试范围、测试资源与时间安排等内容。 2. 测试用例:包括功能测试用例、性能测试用例、安全测试用例等各类测试用例。 3. 测试环境:包括硬件环境、软件环境、网络环境、数据环境等测试所需要的各种环境。 4. …

Chrome提示(已屏蔽:mixed-content)

这是提示信息&#xff0c;它的含义就是你在https的站点里面去请求了非https的资源&#xff0c;(我这里就是这个情况&#xff0c;web页是https的&#xff0c;但是接口是http的)&#xff0c;所以就会出现这个问题&#xff0c;解决办法也很简单&#xff0c;给它套上证书就行了

【考研数学】武忠祥「基础篇」如何衔接进入强化?

如果基础篇已经做完&#xff0c;并且讲义上的例题也都做完了&#xff0c; 那下一步就是该做题了 这个时候&#xff0c;不能盲目做题&#xff0c;做什么题很重要&#xff01;我当初考研之前&#xff0c;基础也很差&#xff0c;所以考研的时候选了错误的题集&#xff0c;做起来就…

有什么方便的教学口语软件?6个软件教你快速练习口语

有什么方便的教学口语软件&#xff1f;6个软件教你快速练习口语 以下是六个方便实用的教学口语软件&#xff0c;它们可以帮助您快速练习口语&#xff1a; AI外语陪练: 这是一款知名的语言学习软件&#xff0c;提供多种语言的口语练习课程。它采用沉浸式的学习方法&#xff0…

020、Python+fastapi,第一个Python项目走向第20步:ubuntu 24.04 docker 安装mysql8集群+redis集群(一)

系列文章 pythonvue3fastapiai 学习_浪淘沙jkp的博客-CSDN博客https://blog.csdn.net/jiangkp/category_12623996.html 前言 docker安装起来比较方便&#xff0c;不影响系统整体&#xff0c;和前面虚拟环境有异曲同工之妙&#xff0c;今天把老笔记本T400拿出来装了个ubuntu24…

HCIP的学习(12)

OSPF优化 ​ OSPF的优化主要目的是为了减少LSA的更新量。 路由汇总-----可以减少骨干区域的LSA数量特殊区域-----可以减少非骨干区域的LSA数量 OSPF路由汇总 域间路由汇总-----在ABR设备上进行操作 [GS-R2-ospf-1-area-0.0.0.1]abr-summary 192.168.0.0 255.255.224.0 [GS-…

分享8000网剧资源

兄弟们&#xff0c;前段时间搞短剧&#xff0c;收集了8500多部网剧资源。搞了整整两个月就赚3块两毛八&#xff0c;电费都不够。还不如进厂打螺丝。果断放弃这项目。 资源在手里面也没啥用。分享出来&#xff0c;大家看着玩。 有其他好的网络项目也可以分享分享。也可也一起…

优化|大语言模型中的优化问题(LoRA相关算法)

一、LoRA 在大语言模型中&#xff0c;参数矩阵 W ∈ R d d W\in \mathbb{R}^{d \times d} W∈Rdd的维度往往可以达到百亿甚至千亿&#xff0c;如果从头开始训练将会特别的消耗时间和资源。因此往往大家都会预先训练好一组初始参数 W 0 ∈ R d d W_0\in \mathbb{R}^{d \times…

git-新增业务代码分支

需求 使用git作为项目管理工具管理项目&#xff0c;我需要有两个分支&#xff0c;一个分支是日常的主分支&#xff0c;会频繁的推送和修改代码并推送另外一个是新的业务代码分支&#xff0c;是一个长期开发的功能&#xff0c;同时这个业务分支需要频繁的拉取主分支的代码&#…