从零开始的数据结构教程(七) 回溯算法
🔄 标题一:回溯核心思想——走迷宫时的“穷举+回头”策略
回溯算法 (Backtracking) 是一种通过探索所有可能的候选解来找出所有的解或某些解的算法。它就像你在一个复杂的迷宫中寻找出路:当你遇到一个岔路口时,你会选择一条路继续走下去;如果走到了死胡同,你就会回溯到上一个岔路口,尝试另一条路。这个过程包含了穷举所有可能性,并在发现无效路径时及时“回头”。
三大核心特征
- 决策树遍历:回溯过程可以被想象成遍历一棵决策树。树的每个节点代表一个决策点,每次选择都相当于从当前节点走向一个子节点。例如,在全排列问题中,每一步你都从剩余的数字中选择一个来填充当前位置。
- 状态回退 (Backtrack):当当前路径无法满足条件(走到死胡同)时,你需要撤销最近的决策,回到上一个决策点,尝试其他分支。这通常通过在递归调用后恢复之前的状态来实现。例如,在 N 皇后问题中,放置皇后后,如果后续无法找到解,就需要移除这个皇后,尝试其他位置。
- 剪枝优化 (Pruning):这是回溯算法的关键优化手段。在决策树的某个节点,如果你能判断出当前分支的后续路径不可能得到有效解,就可以提前终止对该分支的探索,避免不必要的计算。例如,在组合总和问题中,如果当前累加的和已经超过了目标值,就没必要继续往下加了。
通用代码模板
回溯算法通常采用递归的方式实现,可以抽象出以下通用模板:
def backtrack(路径, 选择列表):# 1. 满足结束条件:找到一个解,将其添加到结果集if 满足结束条件:结果.append(路径.copy()) # 注意:这里必须是深拷贝,否则后续路径修改会影响已保存的结果return# 2. 遍历所有可能的选择for 选择 in 选择列表:# 3. 剪枝:如果当前选择不合法(不满足约束条件),则跳过if 选择不合法:continue# 4. 做选择:将当前选择添加到路径中路径.add(选择) # 或者 path.append(选择) 等# 5. 递归:进入下一个决策层backtrack(路径, 新选择列表) # 新选择列表可能根据当前选择更新# 6. 状态回退:撤销当前选择,为下一次循环做准备路径.remove(选择) # 或者 path.pop() 等
♟️ 标题二:排列/组合问题——彩票号码生成器
排列和组合是回溯算法最基础也最常见的应用场景。它们之间的核心区别在于是否考虑元素的顺序以及是否允许元素重复使用。
全排列(LeetCode 46)
- 问题:给定一个不含重复数字的数组
nums
,返回其所有可能的全排列。 - 特点:每个元素只能使用一次,顺序不同算作不同排列。
def permute(nums):res = [] # 存储所有结果的列表n = len(nums)# backtrack 函数:# path: 当前已经形成的排列# used: 记录哪些数字已经被使用过,用集合(set)方便快速查找和删除def backtrack(path, used):# 满足结束条件:当路径的长度等于原数组长度时,说明一个排列已完成if len(path) == n:res.append(path.copy()) # 注意深拷贝return# 遍历所有可能的选择for num in list(used): # 遍历可用数字的副本,因为循环内会修改 used# 做选择:将当前数字添加到路径path.append(num)# 更新选择列表:从可用数字中移除当前数字used.remove(num)# 递归:进入下一个决策层backtrack(path, used)# 状态回退:撤销选择,恢复可用数字path.pop()used.add(num)backtrack([], set(nums)) # 初始调用:空路径,所有数字都可用return res# 示例
# print(permute([1, 2, 3]))
# 输出: [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
- 关键区别:排列问题在每次递归时,都从所有未使用的数字中选择一个。而组合问题通常通过限制遍历的起始索引来避免重复组合和处理顺序。
组合总和(LeetCode 39)
- 问题:给定一个无重复元素的数组
candidates
和一个目标和target
。找出candidates
中所有可以使数字和为target
的组合。candidates
中的数字可以无限制重复被选取。 - 特点:元素可以重复使用,组合不考虑顺序。
def combinationSum(candidates, target):res = []candidates.sort() # 排序是关键剪枝,方便后续判断和跳过# backtrack 函数:# start: 当前轮次开始遍历 candidates 的索引,用于避免重复组合# path: 当前已经形成的组合# remain: 还需要凑齐的剩余目标值def backtrack(start, path, remain):# 满足结束条件:找到一个解if remain == 0:res.append(path.copy())return# 剪枝:如果剩余值小于0,说明当前路径无法达到目标值,直接返回if remain < 0:return# 遍历所有可能的选择(从 start 索引开始,避免重复组合)for i in range(start, len(candidates)):# 剪枝:如果当前候选数已经大于剩余目标值,则后续的数也肯定大于,直接中断循环if candidates[i] > remain:break # 因为 candidates 已排序# 做选择:将当前数添加到路径path.append(candidates[i])# 递归:进入下一个决策层。注意这里递归调用时传入的是 `i` 而不是 `i+1`,# 允许当前数字重复选取backtrack(i, path, remain - candidates[i])# 状态回退:撤销选择path.pop()backtrack(0, [], target) # 初始调用:从索引0开始,空路径,目标值为 targetreturn res# 示例
# print(combinationSum([2, 3, 6, 7], 7))
# 输出: [[2, 2, 3], [7]]
👑 标题三:N 皇后问题——棋盘上的冲突检测
N 皇后问题是回溯算法的经典应用,它完美展示了如何通过递归和剪枝来解决约束满足问题。
问题变形
- 经典 N 皇后:在一个 N × N N \times N N×N 的棋盘上放置
N
个皇后,使得它们之间互不攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。 - 数独求解器(LeetCode 37):同样是基于回溯,通过尝试填充数字并检查约束来解决数独。通常会结合位运算等技巧来优化冲突检测。
冲突检测优化
在 N 皇后问题中,高效地判断一个位置是否能放置皇后是关键。除了使用布尔数组或集合记录已占用的行/列/对角线外,还可以利用数学关系来优化:
- 列冲突:
col
集合记录已占用的列索引。 - 主对角线冲突 (从左上到右下):同一主对角线上的
(row, col)
满足row - col
为常数。 - 副对角线冲突 (从右上到左下):同一副对角线上的
(row, col)
满足row + col
为常数。
def solveNQueens(n):res = [] # 存储所有解决方案# 记录已占用的列、主对角线、副对角线cols = set() # 记录已占用的列索引diag1 = set() # 记录已占用的主对角线索引 (row - col)diag2 = set() # 记录已占用的副对角线索引 (row + col)# path 用二维列表表示棋盘,'.' 为空,'Q' 为皇后# 初始化一个 n*n 的棋盘,全部填充 '.'board = [['.'] * n for _ in range(n)]# backtrack 函数:尝试在当前行放置皇后# row: 当前正在考虑放置皇后的行def backtrack(row):# 满足结束条件:所有行都已成功放置皇后if row == n:# 将当前棋盘(board)转换为字符串列表形式,并添加到结果res.append(["".join(r) for r in board])return# 遍历当前行的所有列,尝试放置皇后for col in range(n):# 剪枝:检查当前位置 (row, col) 是否会发生冲突if col in cols or (row - col) in diag1 or (row + col) in diag2:continue # 如果冲突,则跳过当前列,尝试下一列# 做选择:放置皇后board[row][col] = 'Q'cols.add(col)diag1.add(row - col)diag2.add(row + col)# 递归:进入下一行,继续放置皇后backtrack(row + 1)# 状态回退:撤销选择,将当前位置的皇后移除,并从集合中移除对应信息board[row][col] = '.'cols.remove(col)diag1.remove(row - col)diag2.remove(row + col)backtrack(0) # 从第 0 行开始尝试放置皇后return res# 示例
# print(solveNQueens(4))
# 输出类似棋盘布局的字符串列表
✂️ 标题四:回溯剪枝实战——火柴拼正方形(LeetCode 473)
火柴拼正方形问题是一个很好的回溯与剪枝结合的例子。它要求你将给定长度的火柴分配到四条边,使得它们能构成一个正方形。
问题转化
- 将数组
matchsticks
分成四组,每组火柴的长度之和都等于正方形的边长(即总和 / 4
)。 - 这本质上是一个多组划分问题,可以用回溯法解决。
剪枝策略
高效的剪枝是解决此问题的关键:
- 初始检查:如果所有火柴的总长度不能被 4 整除,或者火柴数量小于 4,则直接返回
False
。 - 排序:将火柴棍按从大到小的顺序排序。这样,长的火柴棍会优先被尝试放置,如果它们无法适应,可以更快地进行剪枝。
- 当前边超长:在尝试放置火柴时,如果当前火柴加上某条边的当前长度超过了目标边长,则直接跳过该火柴。
- 跳过重复状态:如果当前火柴尝试放在
sides[j]
后失败了,那么当sides[j]
与sides[j-1]
相等时,再次尝试将当前火柴放在sides[j-1]
会导致重复的搜索路径,可以跳过。这要求sides
数组在每次递归前都是有序的,或者通过其他方式避免重复尝试。
def makesquare(matchsticks):total = sum(matchsticks)if total % 4 != 0 or len(matchsticks) < 4:return Falseside = total // 4 # 计算正方形的边长matchsticks.sort(reverse=True) # 关键剪枝:从大到小排序火柴棍# sides 数组:表示四条边的当前长度# 初始化为 [0, 0, 0, 0]sides = [0] * 4# backtrack 函数:尝试将第 i 根火柴放到四条边中的一条# i: 当前正在考虑的火柴棍索引# sides: 四条边的当前长度def backtrack(i):# 满足结束条件:所有火柴都已成功放置if i == len(matchsticks):# 检查四条边的长度是否都等于目标边长return all(s == side for s in sides)# 遍历四条边,尝试将当前火柴放入其中for j in range(4):# 剪枝1:如果将当前火柴放入 sides[j] 会使该边超长,则跳过if sides[j] + matchsticks[i] > side:continue# 剪枝2:如果当前边 sides[j] 和前一条边 sides[j-1] 的长度相同,# 且前一条边在尝试放置当前火柴后失败了,那么再次尝试放在这条相同的边上也会失败。# 这有助于避免重复的搜索路径。这个剪枝的前提是 `sides` 数组是排序的,# 但在这里,`sides` 只是记录每条边的累加长度,不是排序的。# 更精确的剪枝是:如果当前 `sides[j]` 的长度和 `sides[j-1]` 相同,# 且它们是空的(即还没开始累加),或者当前火柴和前一个火柴相同,# 可以考虑跳过。但最简单的形式就是只看 `sides[j]` 的值。# 这里的 `j > 0 and sides[j] == sides[j-1]` 剪枝,# 实际上是利用了 `sides` 数组的相对顺序来避免重复计算,# 只有当 `sides` 是有序处理时才有效。# 在本例中,因为火柴是倒序排的,这个剪枝可能需要更精细的判断。# 最简单有效的剪枝是直接检查 `sides[j] + matchsticks[i] > side`。# 为避免误解,我们暂时移除 `j > 0 and sides[j] == sides[j-1]` 剪枝,# 或者强调其适用场景和条件。在这里,更通用且安全的剪枝是 `j > 0 and sides[j] == sides[j-1]` # 只有当 `sides` 数组中的元素(代表边长)是唯一值时才考虑。# 对于本问题,通常不进行此剪枝,或使用更严格的条件。# 做选择:将当前火柴添加到 sides[j]sides[j] += matchsticks[i]# 递归:尝试放置下一根火柴if backtrack(i + 1):return True # 如果找到了一个解决方案,则直接返回 True# 状态回退:撤销选择,将当前火柴从 sides[j] 中移除sides[j] -= matchsticks[i]return False # 如果所有边都尝试过,仍无法放置当前火柴,则返回 Falsereturn backtrack(0) # 从第一根火柴(索引0)开始
📊 总结表:回溯问题类型
回溯算法的应用广泛,通常可以根据问题类型来划分:
问题类型 | 典型例题 | 剪枝技巧 |
---|---|---|
排列问题 | 全排列 II(LeetCode 47) | 排序后跳过重复数字,防止生成重复排列。 |
子集问题 | 子集(LeetCode 78)、组合(LeetCode 77) | 限制遍历的起始索引,确保组合唯一且避免重复。 |
分割问题 | 分割回文串(LeetCode 131) | 预处理所有子串的回文判断,避免重复计算。 |
棋盘/矩阵问题 | 解数独(LeetCode 37)、N 皇后 | 利用行/列/对角线/宫格标记已用数字,或位运算优化冲突检测。 |
组合优化 | 组合总和 II(LeetCode 40)、火柴拼正方形 | 排序输入,提前剪枝不符合条件的路径;跳过重复的决策分支。 |
相关文章:
从零开始的数据结构教程(七) 回溯算法
🔄 标题一:回溯核心思想——走迷宫时的“穷举回头”策略 回溯算法 (Backtracking) 是一种通过探索所有可能的候选解来找出所有的解或某些解的算法。它就像你在一个复杂的迷宫中寻找出路:当你遇到一个岔路口时,你会选择一条路继续…...

CentOS-stream-9 Zabbix的安装与配置
一、Web环境搭建部署Zabbix时,选择合适的MariaDB、PHP和Nginx版本非常重要,以确保兼容性和最佳性能。以下是建议版本:Zabbix 6.4 MariaDB:官方文档推荐使用MariaDB 10.3或更高版本。对于CentOS Stream 9,建议使用Maria…...

开源是什么?我们为什么要开源?
本片为故事类文章推荐听音频哦 软件自由运动的背景 梦开始的地方 20世纪70年代,软件行业处于早期发展阶段,软件通常与硬件捆绑销售,用户对软件的使用、修改和分发权利非常有限。随着计算机技术的发展和互联网的普及,越来越多的开…...

【unity游戏开发——编辑器扩展】EditorApplication公共类处理编辑器生命周期事件、播放模式控制以及各种编辑器状态查询
注意:考虑到编辑器扩展的内容比较多,我将编辑器扩展的内容分开,并全部整合放在【unity游戏开发——编辑器扩展】专栏里,感兴趣的小伙伴可以前往逐一查看学习。 文章目录 前言一、监听编辑器事件1、常用编辑器事件2、示例监听播放模…...
elasticsearch低频字段优化
在Elasticsearch中,通过设置"index": false关闭低频字段的倒排索引构建是常见的优化手段,以下是关键要点: 一、核心机制 倒排索引禁用 设置index: false后,字段不会生成倒排索引,无法通过常规查…...

React---day3
React 2.5 jsx的本质 jsx 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。所有的jsx最终都会被转换成React.createElement的函数调用。 createElement需要传递三个参数: 参数一:type 当前ReactElement的类型;…...

PyCharm接入DeepSeek,实现高效AI编程
介绍本土AI工具DeepSeek如何结合PyCharm同样实现该功能。 一 DeepSeek API申请 首先进入DeepSeek官网:DeepSeek 官网 接着点击右上角的 “API 开放平台“ 然后点击API keys 创建好的API key,记得复制保存好 二 pycharm 接入deepseek 首先打开PyCh…...
前端面经 get和post区别
get获取数据 post提交资源,引起服务器状态变化或者副作用 区别 1get会比post更不安全 get参数写在url中 post在请求体内 2get报文 head和body一起发 响应200 post报文 先发head 100 再发 body 200 3 get请求url有长度限制 4 默认缓存get 请求...

CTFSHOW-WEB-36D杯
给你shell 这道题对我这个新手还是有难度的,花了不少时间。首先f12看源码,看到?view_source,点进去看源码 <?php //Its no need to use scanner. Of course if you want, but u will find nothing. error_reporting(0); include "…...
MySQL connection close 后, mysql server上的行为是什么
本文着重讲述的是通过 msql client 连接到 mysql server ,发起 update 、 select 操作(由于数据量非常大,所以 update、select 操作都很耗时,即在结果返回前我们有足够的时间执行一些操作) 。 在客户端分别尝试执行 ctrl C 结束关闭 mysql c…...

RabbitMQ vs MQTT:深入比较与最新发展
RabbitMQ vs MQTT:深入比较与最新发展 引言 在消息队列和物联网(IoT)通信领域,RabbitMQ 和 MQTT 是两种备受瞩目的技术,各自针对不同的需求和场景提供了强大的解决方案。随着 2025 年的到来,这两项技术都…...

金砖国家人工智能高级别论坛在巴西召开,华院计算应邀出席并发表主题演讲
当地时间5月20日,由中华人民共和国工业和信息化部,巴西发展、工业、贸易与服务部,巴西公共服务管理和创新部以及巴西科技创新部联合举办的金砖国家人工智能高级别论坛,在巴西首都巴西利亚举行。 中华人民共和国工业和信息化部副部…...

【KWDB 创作者计划】_再热垃圾发电汽轮机仿真与监控系统:KaiwuDB 批量插入10万条数据性能优化实践
再热垃圾发电汽轮机仿真与监控系统:KaiwuDB 批量插入10万条数据性能优化实践 我是一台N25-3.82/390型汽轮机,心脏在5500转/分的轰鸣中跳动。垃圾焚烧炉是我的胃,将人类遗弃的残渣转化为金色蒸汽,沿管道涌入我的胸腔。 清晨&#x…...
CentOS 7 安装docker缺少slirp4netnsy依赖解决方案
CentOS 7安装docker缺少slirp4netnsy依赖解决方案 Error: Package: docker-ce-rootless-extras-26.1.4-1.el7.x86_64 (docker-ce-stable) Requires: slirp4netns > 0.4 Error: Package: docker-ce-rootless-extras-26.1.4-1.el7.x86_64 (docker-ce-stable) 解决方案 若wge…...

Android第十一次面试多线程篇
面试官: “你在项目里用过Handler吗?能说说它是怎么工作的吗?” 候选人: “当然用过!比如之前做下载功能时,需要在后台线程下载文件,然后在主线程更新进度条。这时候就得用Handler来切…...

安全,稳定可靠的政企即时通讯数字化平台
在当今数字化时代,政企机构面临着复杂多变的业务需求和日益增长的沟通协作挑战。BeeWorks作为一款安全,稳定可靠的政企即时通讯数字化平台,凭借其安全可靠、功能强大的特性,为政企提供了高效、便捷的沟通协作解决方案,…...
craw4ai 抓取实时信息,与 mt4外行行情结合实时交易,基本面来觉得趋势方向,搞一个外汇交易策略
结合实时信息抓取、MT4行情数据、基本面分析的外汇交易策略框架,旨在通过多维度数据融合提升交易决策质量:行不行不知道先试试,理论是对的,只要基本面方向没错 策略名称:Tri-Sync 外汇交易系统 核心理念 「基本面定方…...
Linux之守护进程
在Linux系统中,进程一般分为前台进程、后台进程和守护进程3类。 一 守护进程 定义: 1.守护进程是在操作系统后台运行的一种特殊类型的进程,它独立于前台用户界面,不与任何终端设备直接关联。这些进程通常在系统启动时启动,并持…...

LiquiGen流体导入UE
导出ABC 导出贴图 ABC导入Houdini UE安装SideFX_Labs插件 C:\Users\Star\Documents\houdini20.5\SideFXLabs\unreal\5.5 参考: LiquiGenHoudiniUE血液流程_哔哩哔哩_bilibili...
使用react进行用户管理系统
今天通了一遍使用react进行用户管理系统的文档,以及跟随步骤实现了一遍,我大概梳理一下实现思路。 首先我们构建基本用户管理应用,需要数据库存储个人资料,我们先去supabase注册然后创建自己的数据库然后设置密码,然后…...
SpringBoot的java应用中,慢sql会导致CPU暴增吗
是的,在 Spring Boot 的 Java 应用中,慢 SQL 同样可能导致 CPU 暴增。虽然数据库服务器的 CPU 通常是主要压力点,但应用服务器(Java 进程)的 CPU 也可能间接受到影响,具体原因和机制如下: 1. 数…...

Ubuntu下编译mininim游戏全攻略
目录 一、安装mininim 软件所依赖的库(重点是allegro游戏引擎库)二、编译mininim 软件三、将mininim打包给另一个Ubuntu系统使用四、安卓手机运行mininim 一、安装mininim 软件所依赖的库(重点是allegro游戏引擎库) 1. 用apt-get…...

uniapp uni-id Error: Invalid password secret
common文件夹下uni-config-center文件夹下新建uni-id,新建config.json文件 复制粘贴以下代码,不要自己改,格式容易错 {"passwordSecret": [{"type": "hmac-sha256","version": 1}], "passwordStrength&qu…...
用 Appuploader,让 iOS 上架流程真正“可交接、可记录、可复用”:我们是这样实现的
你可能听说过这样一类人:上线必找他,证书只有他有,Transporter 密码在他电脑上,描述文件什么时候过期,只有他知道。 如果你团队里有这样一位“发布大师”,他可能是个英雄——但也是个单点风险源。 我们团…...

第十二节:第三部分:集合框架:List系列集合:特点、方法、遍历方式、ArrayList集合的底层原理
List系列集合特点 List集合的特有方法 List集合支持的遍历方式 ArrayList集合的底层原理 ArrayList集合适合的应用场景 代码:List系列集合遍历方式 package com.itheima.day19_Collection_List;import java.util.ArrayList; import java.util.Iterator; import jav…...

【办公类-18-07】20250527屈光检查PDF文件拆分成多个pdf(两页一份,用幼儿班级姓名命名文件)
背景需求: 今天春游,上海海昌公园。路上保健老师收到前几天幼儿的屈光视力检查单PDF。 她说:所有孩子的通知都做在一个PDF里,我没法单独发给班主任。你有什么办法拆开来? 我说:“没问题,问deep…...

AI Agent的“搜索大脑“进化史:从Google API到智能搜索生态的技术变革
AI Agent搜索革命的时代背景 2025年agent速度发展之快似乎正在验证"2025年是agent元年"的说法,而作为agent最主要的应用工具之一(另外一个是coding),搜索工具也正在呈现快速的发展趋势。Google在2024年12月推出Gemini Deep Research࿰…...

Arduino学习-跑马灯
1、效果 2、代码 /**** 2025-5-30 跑马灯的小程序 */ //时间间隔 int intervaltime200; //初始化函数 void setup() {// put your setup code here, to run once://设置第3-第7个引脚为输出模式for(int i3;i<8;i){pinMode(i,OUTPUT);} }//循环执行 void loop() {// put you…...
python创建args命令行分析
这段代码是一个使用 Python 的 argparse 模块创建命令行界面的示例。它定义了一系列的命令行参数和子命令,通常用于构建和管理软件项目或版本控制系统中的操作。以下是对代码的逐行分析: 1初始化 ArgumentParser parser argparse.ArgumentParser(forma…...

2. 手写数字预测 gui版
2. 手写数字预测 gui版 背景1.界面绘制2.处理图片3. 加载模型4. 预测5.结果6.一点小问题 背景 做了手写数字预测的模型,但是老是跑模型太无聊了,就配合pyqt做了一个可视化界面出来玩一下 源代码可以去这里https://github.com/Leezed525/pytorch_toy拿 …...