深入理解回溯算法

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

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

相关文章

【linuxC语言】守护进程

文章目录 前言一、守护进程的介绍二、开启守护进程总结 前言 在Linux系统中&#xff0c;守护进程是在后台运行的进程&#xff0c;通常以服务的形式提供某种功能&#xff0c;如网络服务、系统监控等。守护进程的特点是在启动时脱离终端并且在后台运行&#xff0c;它们通常不与用…

【Linux】冯·诺依曼体系结构

要想谈进程&#xff0c;我们就不能只谈进程&#xff0c;我们如果想搞清楚什么是进程&#xff0c;就要从操作系统讲起。我们现在的不管是Linux或是Windows或是安卓等操作系统&#xff0c;它们都有一个相同点&#xff0c;那就是遵循冯诺依曼体系结构&#xff0c;我们看一下冯诺依…

多用户商城系统思维导图

多用户商城系统思维导图之退换货商品计算情况&#xff1a; 这是多用户商城系统思维导图的子图&#xff0c;总图可参考&#xff1a;

C语言知识点补充——操作符详解

1、计算幂次数和平方根 使用<math.h>数学库 pow()函数计算幂次数&#xff1b;sqrt()函数计算平方根。 注&#xff1a;sqrt()输入同样的数字&#xff0c;计算出来的数值&#xff0c;可能不相等&#xff0c;因为输出double数&#xff0c;小数点后面的数值不一定一致。 2…

mysql数据库(排序与分页)

目录 一. 排序数据 1.1 排序规则 1.2 单列排序 1.我们也可以使用列的别名&#xff0c;给别名进行排序 2.列的别名只能在 ODER BY 中使用&#xff0c; 不能在WHERE中使用。 3.强调格式&#xff1a;WHERE 需要在 FROM 后&#xff0c; ORDER BY 之前 1.3 二级排序&…

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

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

1078 字符串压缩与解压

solution 输入的字符串含空格&#xff0c;可以先吸收换行符再用getline()解压时&#xff0c;字符数可能不止是各位数字&#xff0c;存在多位数的情况压缩时&#xff0c;别漏了最后一组字符 #include<iostream> #include<string> using namespace std; int main()…

SpringBoot自动连接数据库的解决方案

在一次学习设计模式的时候&#xff0c;沿用一个旧的boot项目&#xff0c;想着简单&#xff0c;就把数据库给关掉了&#xff0c;结果报错 Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. 没有数据库的需…

一款开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验

Snap.Hutao 胡桃工具箱是一款以 MIT 协议开源的原神工具箱&#xff0c;专为现代化 Windows 平台设计&#xff0c;旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新功能相结合&#xff0c;提供了一套完整且实用的工具集&#xff0c;且无需依赖任何移动设…

ICode国际青少年编程竞赛- Python-1级训练场-变量入门

ICode国际青少年编程竞赛- Python-1级训练场-变量入门 1、 a 4 Dev.turnRight() Dev.step(a)2、 a 4 Spaceship.step(a) Dev.step(a)3、 a 4 Dev.step(a) Dev.turnLeft() Dev.step(a)4、 a 5 Dev.step(a) Spaceship.step(a) Dev.step(a)5、 a 3 Dev.step(a) Dev.tur…

【如此简单!数据库入门系列】之存储设备简介

文章目录 1 前言2 存储设备分类3 主存层次结构4 磁盘结构5 RAID6 总结7 系列文章 1 前言 没有存储&#xff0c;就没有数据&#xff01; 如果说ER模型和数据库规范化是数据库概念模式的技术和方法&#xff0c;那么存储设备就是数据库物理模式的基础。 物理存储设备包含哪些类型…

风扇开启执行逻辑

执行流程 public static void businessExecutionWork(){//以下为业务逻辑部分System.out.println("1、根据电池包控制风扇服务执行 开始!");//1、获取电池包电压、电流、环境温度//获取电池包电压、电流、环境温度ObtainBatteryDataService obtainBatteryDataServic…

【目标检测】Deformable DETR

一、前言 论文&#xff1a; Deformable DETR: Deformable Transformers for End-to-End Object Detection 作者&#xff1a; SenseTime Research 代码&#xff1a; Deformable DETR 特点&#xff1a; 提出多尺度可变形注意力 (Multi-scale Deformable Attention) 解决DETR收敛…

柯桥西语培训之在西班牙旅游点菜哪些坑不能踩?

Por muy bien que se coma en Espaa —que es mucho— hay una cosa innegable: lo que pasa en la cocina se queda en la cocina. No todos los alimentos son igualmente seguros o sabrosos cuando se encuentran fuera de la comodidad de nuestra propia casa. Ya sea po…

数据库大作业——基于qt开发的图书管理系统 (一)环境的配置与项目需求的分析

前言 博主最近数据库原理结课要做课程设计了,要求开发基于数据库实现的图书管理系统&#xff0c;博主想了想决定做一个基于Qt的图书管理系统,博主在此之前其实也没有用过多少Qt&#xff0c;仅以此专栏记录博主学习与开发的全过程&#xff0c;大家一起学习&#xff0c;一起进步…

DS二叉搜索树

前言 我们在数据结构初阶专栏已经对二叉树进行了介绍并用C语言做了实现&#xff0c;但是当时没有对二叉搜树进行介绍&#xff0c;而是把他放到数据结构进阶构专栏的第一期来介绍&#xff0c;原因是后面的map和set&#xff08;红黑树&#xff09;是基于搜索树的&#xff0c;这里…

Shell脚本编写-定时清空文件内容,定时记录文件内容大小

find命令 – 根据路径和条件搜索指定文件 – Linux命令大全(手册)find命令的功能是根据给定的路径和条件查找相关文件或目录&#xff0c;其参数灵活方便&#xff0c;且支持正则表达式&#xff0c;结合管道符后能够实现更加复杂的功能&#xff0c;是Linux系统运维人员必须掌握的…

学习Rust的第29天: cat in Rust

今天即将是这个系列的最后一次内容&#xff0c;我们正在catRust 中从 GNU 核心实用程序进行重建。cat用于将文件内容打印到STDOUT.听起来很容易构建&#xff0c;所以让我们开始吧。 GitHub 存储库&#xff1a;GitHub - shafinmurani/gnu-core-utils-rust 伪代码 function read(…

省公派出国|社科类普通高校教师限期内赴英国访学交流

在国外访问学者申请中&#xff0c;人文社科类相对难度更大&#xff0c;尤其是英语语言学&#xff0c;作为非母语研究并不被国外高校看重。经过努力&#xff0c;最终我们帮助Z老师申请到英国坎特伯雷基督教会大学的访学职位&#xff0c;并在限期内出国。 Z老师背景&#xff1a; …

TypeScript学习日志-第二十天(模块解析)

模块解析 一、ES6之前的模块规范 前端模块化规范是有很多的&#xff0c;在es6模块化规范之前分别有一下的模块化规范 一、Commonjs 这是 NodeJs 里面的模块化规范 // 导入 require("xxx"); require("../xxx.js"); // 导出 exports.xxxxxx function() …