从零开始的数据结构教程(七) 回溯算法

news2025/6/3 16:58:55

🔄 标题一:回溯核心思想——走迷宫时的“穷举+回头”策略

回溯算法 (Backtracking) 是一种通过探索所有可能的候选解来找出所有的解或某些解的算法。它就像你在一个复杂的迷宫中寻找出路:当你遇到一个岔路口时,你会选择一条路继续走下去;如果走到了死胡同,你就会回溯到上一个岔路口,尝试另一条路。这个过程包含了穷举所有可能性,并在发现无效路径时及时“回头”。

三大核心特征

  1. 决策树遍历:回溯过程可以被想象成遍历一棵决策树。树的每个节点代表一个决策点,每次选择都相当于从当前节点走向一个子节点。例如,在全排列问题中,每一步你都从剩余的数字中选择一个来填充当前位置。
  2. 状态回退 (Backtrack):当当前路径无法满足条件(走到死胡同)时,你需要撤销最近的决策,回到上一个决策点,尝试其他分支。这通常通过在递归调用后恢复之前的状态来实现。例如,在 N 皇后问题中,放置皇后后,如果后续无法找到解,就需要移除这个皇后,尝试其他位置。
  3. 剪枝优化 (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开始,空路径,目标值为 target
    return 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)。
  • 这本质上是一个多组划分问题,可以用回溯法解决。

剪枝策略

高效的剪枝是解决此问题的关键:

  1. 初始检查:如果所有火柴的总长度不能被 4 整除,或者火柴数量小于 4,则直接返回 False
  2. 排序:将火柴棍按从大到小的顺序排序。这样,长的火柴棍会优先被尝试放置,如果它们无法适应,可以更快地进行剪枝。
  3. 当前边超长:在尝试放置火柴时,如果当前火柴加上某条边的当前长度超过了目标边长,则直接跳过该火柴。
  4. 跳过重复状态:如果当前火柴尝试放在 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 False

    side = 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 # 如果所有边都尝试过,仍无法放置当前火柴,则返回 False

    return backtrack(0) # 从第一根火柴(索引0)开始

📊 总结表:回溯问题类型

回溯算法的应用广泛,通常可以根据问题类型来划分:

问题类型典型例题剪枝技巧
排列问题全排列 II(LeetCode 47)排序后跳过重复数字,防止生成重复排列。
子集问题子集(LeetCode 78)、组合(LeetCode 77)限制遍历的起始索引,确保组合唯一且避免重复。
分割问题分割回文串(LeetCode 131)预处理所有子串的回文判断,避免重复计算。
棋盘/矩阵问题解数独(LeetCode 37)、N 皇后利用行/列/对角线/宫格标记已用数字,或位运算优化冲突检测。
组合优化组合总和 II(LeetCode 40)、火柴拼正方形排序输入,提前剪枝不符合条件的路径;跳过重复的决策分支。

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

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

相关文章

CentOS-stream-9 Zabbix的安装与配置

一、Web环境搭建部署Zabbix时&#xff0c;选择合适的MariaDB、PHP和Nginx版本非常重要&#xff0c;以确保兼容性和最佳性能。以下是建议版本&#xff1a;Zabbix 6.4 MariaDB&#xff1a;官方文档推荐使用MariaDB 10.3或更高版本。对于CentOS Stream 9&#xff0c;建议使用Maria…

开源是什么?我们为什么要开源?

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

【unity游戏开发——编辑器扩展】EditorApplication公共类处理编辑器生命周期事件、播放模式控制以及各种编辑器状态查询

注意&#xff1a;考虑到编辑器扩展的内容比较多&#xff0c;我将编辑器扩展的内容分开&#xff0c;并全部整合放在【unity游戏开发——编辑器扩展】专栏里&#xff0c;感兴趣的小伙伴可以前往逐一查看学习。 文章目录 前言一、监听编辑器事件1、常用编辑器事件2、示例监听播放模…

React---day3

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

PyCharm接入DeepSeek,实现高效AI编程

介绍本土AI工具DeepSeek如何结合PyCharm同样实现该功能。 一 DeepSeek API申请 首先进入DeepSeek官网&#xff1a;DeepSeek 官网 接着点击右上角的 “API 开放平台“ 然后点击API keys 创建好的API key&#xff0c;记得复制保存好 二 pycharm 接入deepseek 首先打开PyCh…

CTFSHOW-WEB-36D杯

给你shell 这道题对我这个新手还是有难度的&#xff0c;花了不少时间。首先f12看源码&#xff0c;看到?view_source&#xff0c;点进去看源码 <?php //Its no need to use scanner. Of course if you want, but u will find nothing. error_reporting(0); include "…

RabbitMQ vs MQTT:深入比较与最新发展

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

金砖国家人工智能高级别论坛在巴西召开,华院计算应邀出席并发表主题演讲

当地时间5月20日&#xff0c;由中华人民共和国工业和信息化部&#xff0c;巴西发展、工业、贸易与服务部&#xff0c;巴西公共服务管理和创新部以及巴西科技创新部联合举办的金砖国家人工智能高级别论坛&#xff0c;在巴西首都巴西利亚举行。 中华人民共和国工业和信息化部副部…

【KWDB 创作者计划】_再热垃圾发电汽轮机仿真与监控系统:KaiwuDB 批量插入10万条数据性能优化实践

再热垃圾发电汽轮机仿真与监控系统&#xff1a;KaiwuDB 批量插入10万条数据性能优化实践 我是一台N25-3.82/390型汽轮机&#xff0c;心脏在5500转/分的轰鸣中跳动。垃圾焚烧炉是我的胃&#xff0c;将人类遗弃的残渣转化为金色蒸汽&#xff0c;沿管道涌入我的胸腔。 清晨&#x…

Android第十一次面试多线程篇

​面试官​&#xff1a; “你在项目里用过Handler吗&#xff1f;能说说它是怎么工作的吗&#xff1f;” ​候选人​&#xff1a; “当然用过&#xff01;比如之前做下载功能时&#xff0c;需要在后台线程下载文件&#xff0c;然后在主线程更新进度条。这时候就得用Handler来切…

安全,稳定可靠的政企即时通讯数字化平台

在当今数字化时代&#xff0c;政企机构面临着复杂多变的业务需求和日益增长的沟通协作挑战。BeeWorks作为一款安全&#xff0c;稳定可靠的政企即时通讯数字化平台&#xff0c;凭借其安全可靠、功能强大的特性&#xff0c;为政企提供了高效、便捷的沟通协作解决方案&#xff0c;…

LiquiGen流体导入UE

导出ABC 导出贴图 ABC导入Houdini UE安装SideFX_Labs插件 C:\Users\Star\Documents\houdini20.5\SideFXLabs\unreal\5.5 参考: LiquiGenHoudiniUE血液流程_哔哩哔哩_bilibili

Ubuntu下编译mininim游戏全攻略

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

uniapp uni-id Error: Invalid password secret

common文件夹下uni-config-center文件夹下新建uni-id,新建config.json文件 复制粘贴以下代码&#xff0c;不要自己改&#xff0c;格式容易错 {"passwordSecret": [{"type": "hmac-sha256","version": 1}], "passwordStrength&qu…

第十二节:第三部分:集合框架:List系列集合:特点、方法、遍历方式、ArrayList集合的底层原理

List系列集合特点 List集合的特有方法 List集合支持的遍历方式 ArrayList集合的底层原理 ArrayList集合适合的应用场景 代码&#xff1a;List系列集合遍历方式 package com.itheima.day19_Collection_List;import java.util.ArrayList; import java.util.Iterator; import jav…

【办公类-18-07】20250527屈光检查PDF文件拆分成多个pdf(两页一份,用幼儿班级姓名命名文件)

背景需求&#xff1a; 今天春游&#xff0c;上海海昌公园。路上保健老师收到前几天幼儿的屈光视力检查单PDF。 她说&#xff1a;所有孩子的通知都做在一个PDF里&#xff0c;我没法单独发给班主任。你有什么办法拆开来&#xff1f; 我说&#xff1a;“没问题&#xff0c;问deep…

AI Agent的“搜索大脑“进化史:从Google API到智能搜索生态的技术变革

AI Agent搜索革命的时代背景 2025年agent速度发展之快似乎正在验证"2025年是agent元年"的说法&#xff0c;而作为agent最主要的应用工具之一(另外一个是coding)&#xff0c;搜索工具也正在呈现快速的发展趋势。Google在2024年12月推出Gemini Deep Research&#xff0…

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…

2. 手写数字预测 gui版

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

特别篇-产品经理(三)

一、市场与竞品分析—竞品分析 1. 课后总结 案例框架&#xff1a;通过"小新吃蛋糕"案例展示行业分析方法&#xff0c;包含四个关键步骤&#xff1a; 明确目标行业调研确定竞品分析竞争策略输出结论 1&#xff09;行业背景分析方法 PEST分析法&#xff1a;从四个…