当前位置: 首页 > news >正文

python 单词搜索(回溯-矩阵-字符串-中等)含源码(二十)

问题说明(含示例)

问题描述:给定一个 m x n 的二维字符网格 board 和一个字符串 word,判断 word 是否存在于网格中。单词需按字母顺序,通过相邻(水平 / 垂直,无对角线)单元格的字母构成,且同一个单元格的字母不允许重复使用。

示例

输入输出解释
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"true路径为 (0,0)→(0,1)→(0,2)→(1,2)→(2,2)→(2,1),匹配所有字符
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE"true路径为 (1,3)→(2,3)→(2,2),匹配所有字符
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB"false无法找到不重复使用单元格且相邻的路径,如第二个 B 无合法相邻单元格

解题关键

核心思路是回溯法,通过 “状态标记 - 相邻探索 - 状态恢复” 实现路径搜索,而回溯的底层依赖(Python 解释器的递归调用栈或手动模拟的列表栈)管理状态。关键步骤如下:

  1. 初始过滤:若网格为空,直接返回 false;遍历网格,仅将与 word[0] 匹配的单元格作为回溯起始点(剪枝优化)。
  2. 方向定义:用 (0,1)、(0,-1)、(-1,0)、(1,0) 表示右、左、上、下四个相邻方向,确保仅按规则探索。
  3. 栈式回溯
    • 递归实现:依赖 Python 底层递归调用栈自动管理状态(参数、局部变量、返回地址);
    • 迭代实现:用列表模拟栈手动存储状态(单元格坐标、匹配索引、已访问集合)。
  4. 状态管理:通过修改网格字符(如用 $ 标记已访问)避免重复使用,探索失败后恢复原字符(回溯核心)。

核心逻辑 + 关键细节(含 Python 栈的内部设定)

一、回溯的核心逻辑:“标记 - 探索 - 恢复” 闭环

无论用递归栈还是手动栈,回溯的核心是通过栈记忆 “当前状态”,确保探索失败后能回退到上一步,具体闭环如下:

  1. 标记:将当前单元格标记为已访问(如 board[i][j] = '$'),避免同一路径重复使用;
  2. 探索:按四个方向遍历相邻单元格,若符合 “不越界、未访问、字符匹配”,则进入下一层探索;
  3. 恢复:若所有方向探索失败,恢复当前单元格的原字符(如 board[i][j] = temp),回退到上一步状态。

二、Python 栈的内部设定(两种实现方式)

栈的核心作用是 **“记忆状态 + 控制回退”**,Python 中存在两种栈的使用场景,内部设定差异显著:

1. 递归实现:依赖 Python 解释器的 “递归调用栈”(自动管理)
(1)栈的内部结构:栈帧(Stack Frame)

每次调用递归函数(如 backtrack(i, j, k)),Python 解释器会自动创建一个栈帧,压入递归调用栈。栈帧包含以下关键信息(用户无需手动定义,由解释器维护):

栈帧内容作用示例(backtrack(0,0,0)
函数参数记录当前探索的单元格坐标(i,j)和匹配进度(ki=0, j=0, k=0
局部变量存储当前单元格的原字符(temp),用于后续恢复temp = 'A'board[0][0] 的原字符)
返回地址记录函数执行完 return 后,需回到的上一层代码位置主函数中 if backtrack(0,0,0): 这一行
(2)栈的操作时机(自动执行)
  • 压栈(Push):调用递归函数时触发。例如,从 backtrack(0,0,0) 调用 backtrack(0,1,1),会将 (i=0,j=1,k=1, temp='B', 返回地址=backtrack(0,0,0)的for循环) 压入栈;
  • 弹栈(Pop):函数执行 return 时触发。例如,backtrack(0,1,1) 探索失败返回 false,其栈帧会从栈中弹出,控制权回到上一层的返回地址(继续遍历下一个方向)。
(3)示例:递归栈的工作流程(匹配 “AB”)
1. 主函数调用 backtrack(0,0,0) → 压栈帧1(i=0,j=0,k=0,temp='A');
2. 探索右方向,调用 backtrack(0,1,1) → 压栈帧2(i=0,j=1,k=1,temp='B');
3. k=1 == len(word)-1(word="AB"),返回 true → 弹栈帧2;
4. 栈帧1接收到true,返回主函数 → 弹栈帧1;
5. 主函数返回 true,流程结束。
2. 迭代实现:用 “列表模拟栈”(手动管理)

若递归深度过大(如网格尺寸 1000x1000),可能触发栈溢出,此时需用列表手动模拟栈,内部设定完全由用户控制:

(1)栈的内部结构:自定义状态元组

列表中每个元素是一个状态元组,包含探索所需的全部信息(需手动定义),格式为 (i, j, k, visited)

状态字段作用示例
i,j当前单元格坐标(0,0)
k当前匹配的 word 索引0(匹配 word[0]
visited已访问的单元格集合(避免重复){(0,0)}
(2)栈的操作时机(手动执行
  • 压栈(Push):用 list.append() 实现,将符合条件的新状态加入列表尾部(栈顶);
  • 弹栈(Pop):用 list.pop() 实现,从列表尾部取出最后压入的状态(栈顶),即 “回退到上一步”。
(3)示例:手动栈的工作流程(匹配 “AB”)
stack = [(0,0,0, {(0,0)})]  # 初始压栈:起始点状态
while stack:i,j,k,visited = stack.pop()  # 弹栈:取栈顶状态if k == 1:  # 匹配完"AB"return Truefor dx,dy in direction:new_i,new_j = i+dx,j+dy# 符合条件:不越界、未访问、字符匹配if 0<=new_i<n and 0<=new_j<m and (new_i,new_j) not in visited and board[new_i][new_j] == word[k+1]:new_visited = visited.copy()  # 复制集合,避免状态污染new_visited.add((new_i,new_j))stack.append((new_i,new_j,k+1,new_visited))  # 压栈新状态
return False

三、关键细节:避坑与优化

  1. 方向数组不可重复:需确保四个方向不重复(如避免 (0,-1) 出现两次),否则会遗漏方向或重复探索;
  2. 边界判断需精准:行索引范围是 0<=i<nn 为网格行数),列索引是 0<=j<mm 为网格列数),避免 i>n 这类错误(会导致越界访问);
  3. 状态恢复的必要性:递归中若不恢复原字符(board[i][j] = temp),会导致后续路径无法复用单元格;迭代中若不复制 visited 集合(直接修改原集合),会导致状态污染;
  4. 剪枝优化:仅从与 word[0] 匹配的单元格开始探索,减少无效递归 / 压栈操作。

对应代码(两种栈实现方式)

1. 递归版(依赖 Python 底层递归调用栈)

from typing import Listclass Solution:def exist(self, board: List[List[str]], word: str) -> bool:# 1. 初始过滤与变量定义n = len(board)if n == 0:return Falsem = len(board[0])direction = [(0,1), (0,-1), (-1,0), (1,0)]  # 右、左、上、下word_len = len(word)# 2. 回溯函数(依赖Python递归调用栈)def backtrack(i: int, j: int, k: int) -> bool:# 终止条件1:匹配完所有字符if k == word_len:return True# 终止条件2:越界、字符不匹配if i < 0 or i >= n or j < 0 or j >= m or board[i][j] != word[k]:return False# 标记:保存原字符,用$标记已访问(栈帧中存储temp)temp = board[i][j]board[i][j] = '$'# 探索四个方向(递归调用,自动压栈)for dx, dy in direction:new_i, new_j = i + dx, j + dyif backtrack(new_i, new_j, k + 1):return True  # 找到有效路径,直接返回(弹栈后向上传递)# 恢复:回溯,恢复原字符(栈帧弹出前执行)board[i][j] = tempreturn False  # 所有方向失败,返回(弹栈)# 3. 遍历起始点,启动回溯for i in range(n):for j in range(m):if board[i][j] == word[0]:if backtrack(i, j, 0):return Truereturn False

2. 迭代版(用列表手动模拟栈)

from typing import Listclass Solution:def exist(self, board: List[List[str]], word: str) -> bool:# 1. 初始过滤与变量定义n = len(board)if n == 0:return Falsem = len(board[0])direction = [(0,1), (0,-1), (-1,0), (1,0)]word_len = len(word)stack = []# 2. 初始化栈:压入所有匹配word[0]的起始点状态for i in range(n):for j in range(m):if board[i][j] == word[0]:stack.append((i, j, 0, {(i, j)}))  # 手动压栈:(坐标, 匹配索引, 已访问集合)# 3. 迭代处理栈(手动弹栈、压栈)while stack:i, j, k, visited = stack.pop()  # 手动弹栈:取栈顶状态# 终止条件:匹配完所有字符if k == word_len:return True# 探索四个方向,手动压栈新状态for dx, dy in direction:new_i = i + dxnew_j = j + dy# 检查:不越界、未访问、字符匹配if 0 <= new_i < n and 0 <= new_j < m:if (new_i, new_j) not in visited and board[new_i][new_j] == word[k + 1]:# 复制已访问集合,避免状态污染new_visited = visited.copy()new_visited.add((new_i, new_j))# 手动压栈:新状态加入栈顶stack.append((new_i, new_j, k + 1, new_visited))# 所有状态探索失败return False

对应的基础知识

1. Python 递归调用栈的内部机制

  • 自动管理:递归调用栈由 Python 解释器底层维护,用户无需手动操作,仅需编写递归逻辑;
  • 栈帧生命周期:函数调用时创建栈帧(压栈),函数返回时销毁栈帧(弹栈),栈帧中的局部变量仅在当前函数调用中有效;
  • 栈溢出风险:Python 默认递归深度约为 1000(可通过 sys.setrecursionlimit() 修改,但不推荐),若网格尺寸过大(如 2000x2000),递归深度可能超过限制,触发 RecursionError

2. 列表模拟栈的原理

  • 数据结构匹配:Python 列表的 append()(尾部添加)和 pop()(尾部删除)操作均为 O(1) 时间复杂度,符合栈 “先进后出(LIFO)” 的特性;
  • 状态独立性:迭代中需复制 visited 集合(如 new_visited = visited.copy()),因为列表中的状态共享引用,直接修改会导致所有相关状态的 visited 被污染;
  • 灵活性:手动栈可自定义状态字段(如增加 “方向索引” 记录已探索的方向),避免重复探索,而递归栈无法直接控制。

3. 二维网格的索引操作

  • 行数与列数n = len(board) 表示网格行数(外层列表长度),m = len(board[0]) 表示网格列数(内层列表长度),需先判断 board 非空再获取 m
  • 相邻坐标计算:通过方向数组 (dx, dy) 计算新坐标 new_i = i + dxnew_j = j + dy,避免硬编码(如 i+1j-1)导致的代码冗余。

对应的进阶知识

1. 两种栈实现的效率对比

对比维度递归调用栈(自动)列表模拟栈(手动)
代码复杂度低(逻辑简洁,无需手动管理状态)高(需手动处理压栈、弹栈、状态复制)
运行效率较低(函数调用有栈帧创建 / 销毁开销)较高(无函数调用开销,仅列表操作)
栈溢出风险高(递归深度受限,大网格易溢出)低(列表大小仅受内存限制,无深度限制)
适用场景小规模网格(如 100x100 以内)、代码可读性优先大规模网格、递归深度超限时

2. 时间与空间复杂度

  • 时间复杂度O(nm×3ᵏ)n= 行数,m= 列数,k=word 长度);每个单元格最多作为起始点一次(O(nm)),每个单元格探索时最多有 3 个有效方向(排除来时的方向),递归 / 迭代深度为 k,故每个起始点的时间为 O(3ᵏ)
  • 空间复杂度
    • 递归版:O(k)(递归栈深度为 k,网格标记无额外空间);
    • 迭代版:O(nm)(最坏情况下栈存储所有单元格状态,visited 集合最大为 nm)。

3. 优化策略:避免重复探索

  • 方向剪枝:迭代版中,可在状态元组中增加 “方向索引”(如 (i,j,k,visited, dir_idx)),记录已探索的方向,下次弹栈时从 dir_idx+1 开始遍历,避免重复探索同一方向;
  • 原地标记 vs 额外集合:递归版用 “原地修改网格” 标记已访问(空间 O(1)),迭代版用 “visited 集合”(空间 O(nm)),前者空间更优,但需确保状态恢复正确。

编程思维与启示

1. “栈” 是回溯的核心载体

回溯的本质是 “深度优先搜索 + 状态回退”,而栈的 “先进后出” 特性完美匹配这一逻辑 —— 栈记住 “当前路径的所有状态”,回退时只需弹出栈顶状态,即可回到上一步,这是 “为什么回溯离不开栈” 的根本原因。

2. “就地修改” 与 “状态隔离” 的平衡

  • 递归版用 “就地修改网格”($ 标记)实现状态标记,通过 “恢复原字符” 实现状态隔离,空间效率高,但需确保每个修改都有对应的恢复;
  • 迭代版用 “复制 visited 集合” 实现状态隔离,无需修改网格,但空间效率低,需在 “空间” 与 “代码复杂度” 间权衡。

3. 问题拆解的简化思路

将 “单词搜索” 拆解为 “起始点筛选→相邻探索→状态管理” 三个子问题,每个子问题用对应的数据结构解决:

  • 起始点筛选:用网格遍历 + 字符匹配(剪枝);
  • 相邻探索:用方向数组 + 边界判断;
  • 状态管理:用栈(自动 / 手动)实现回溯。这种 “分而治之” 的思维可迁移到 “岛屿数量”“路径总和” 等网格类问题。
http://www.dtcms.com/a/495317.html

相关文章:

  • hot 100 | 一文讲清动态规划
  • 操作简单稳定选巨 椰 云手机
  • 设计模式之:工厂方法模式
  • 西宁市精神文明建设网站餐饮店面装饰设计
  • 对营销网站建设评估及分析佛山顺德做网站
  • 高并发系统架构设计原则:无状态、水平扩展、异步化、缓存优先
  • 系统架构设计师备考第45天——软件架构演化评估方法和维护
  • 基于SpringBoot+Vue的社区诊所管理系统(AI问答、webSocket实时聊天、Echarts图形化分析)
  • 【MySQL】第二章 基本的SELECT语句
  • Linux中软中断tasklet任务队列初始化
  • 网站制作的重要流程世界优秀摄影作品网站
  • 技术剖析:智能体工作流与RPA流程自动化的架构差异与融合实现
  • 深圳比斯特自动化|圆柱电池测试设备核心功能与技术发展解析
  • 【软考备考】系统架构设计需要考虑的因素 性能 、安全、成本、可维护性详解知识点五
  • 面试反馈 Spring Cloud 的25连环炮
  • 第八篇: `lsmod`, `modinfo`, `modprobe` - 深入内核模块
  • aspx网站服务器失去响应天工网工程新希望官网
  • 网站服务器要多少钱【邯郸网络推广公司|邯郸网络营销公司】
  • 做网站用什么域名比较好找公司开发网站
  • 【Python】求解GPS未知及高斯噪声
  • Linux 教程:如何查找服务器中的大文件
  • 计算机网络基础篇——应用层
  • 2025年主流外贸管理软件深度测评和选型策略咨询报告
  • 玩Android Harmony next版,通过项目了解harmony项目快速搭建开发
  • 公司免费网站域名申请免费网址
  • 华为鲲鹏 Aarch64 环境下多 Oracle 数据库汇聚操作指南 CMP(类 Cloudera CDP 7.3)
  • Chromedriver放项目里就行!Selenium 3 和 4 指定路径方法对比 + 兼容写法
  • Spring Boot 项目, idea 控制台日志设置彩色
  • wap网站建设好不好百度小程序官方收费标准
  • Go语言数据竞争Data Race 问题怎么检测?怎么解决?